;======================================================================
; POPWATCH 1.00 Copyright (c) 1992, Robert L. Hummel
; PC Magazine Assembly Language Lab Notes
;
; POPWATCH is a memory resident clock, calendar, and stopwatch. It may
; be used interactively by popping up the POPWATCH window using the
; ALT-P keystroke combination. Alternately, it can be invoked from the
; command line or from a batch file to produce date and time stamps
; that can be directed to the active display or to the printer.
;----------------------------------------------------------------------
CSEG		SEGMENT	PARA	PUBLIC	'CODE'
	ASSUME	CS:CSEG, DS:CSEG, ES:CSEG, SS:CSEG

		ORG	100H			;COM file format
ENTPT:		JMP	MAIN

;======================================================================
; General program equates.
;----------------------------------------------------------------------
CR		EQU	0DH		;ASCII carriage return
LF		EQU	0AH		;ASCII line feed
SPACE		EQU	20H		;ASCII blank
ESCAPE		EQU	1BH		;ASCII ESC
ENDKEY		EQU	4F00H		;END key scan code
JMPFAR		EQU	0EAH		;Opcode for jump far

SHIFT_MASK	EQU	08H		;Mask to pick out ALT
HOTKEY		EQU	19H		;SCAN code for P key

;======================================================================
; Resident data area - this data remains with the resident portion.
;----------------------------------------------------------------------
; Messages.
;----------------------------------------------------------------------
RES_MARKER	DB	CR,LF,"PopWatch 1.00 Copyright (c) 1992 ",254
		DB	" Robert L. Hummel",26
MARKER_LEN	EQU	$-RES_MARKER
;----------------------------------------------------------------------
; Program variables.
;----------------------------------------------------------------------
DOS_BUSY	DD	0		;Address of DOS busy flag
REQUEST		DB	0		;Non-zero if request pending
ACTIVE		DB	0		;Nonzero if POPWATCH is active
;----------------------------------------------------------------------
; Video data.
;----------------------------------------------------------------------
NROW		EQU	12		;Number of rows in the window
NCOL		EQU	22		;Number of cols in the window
BOX_ROW		EQU	02		;Top row of window on screen
BOX_COL		EQU	05		;Left col of window on screen

CUR_POS		DW	0		;Original cursor position
CUR_SIZ		DW	0		;Original cursor shape
VPAGE		DB	0		;Current video page
VATTR		DB	0		;Window color
 BW_ATTR	EQU	70H		;Monochrome window
 CO_ATTR	EQU	17H		;Color window

;======================================================================
; INT_9 (ISR)
;
; This routine gets control on each keypress and tests to see if the
; hotkey combination has been typed. If so, it sets a flag indicating
; that POPWATCH should be activated.
;----------------------------------------------------------------------
INT_9		PROC	FAR
	ASSUME	CS:CSEG, DS:NOTHING, ES:NOTHING, SS:NOTHING

		PUSH	AX			;Save used register
;----------------------------------------------------------------------
; Read the key scan code from the controller and compare to the scan
; code for our hotkey.
;----------------------------------------------------------------------
		IN	AL,60H			;Get key scan code
		CMP	AL,HOTKEY		;Check if hot-key
		JNE	I9_1
;----------------------------------------------------------------------
; Determine if the ALT key is also pressed.
;----------------------------------------------------------------------
		STI				;Enable interrupts
		MOV	AH,2			;Get shift status fn
		INT	16H			; thru BIOS

		AND	AL,0FH			;Test shift keys
		CMP	AL,SHIFT_MASK		;Must match exactly
		JE	I9_2
;----------------------------------------------------------------------
; Restore the altered register and jump to the original Int 9 handler.
;----------------------------------------------------------------------
I9_1:
		POP	AX			;Restore register
		CLI				;Interrupts off

		DB	JMPFAR			;Jump to old interrupt
OLDINT9		DD	-1			; located here
;----------------------------------------------------------------------
; Reset the keyboard and interrupt controllers. This tells the system
; to simply forget the key stroke.
;----------------------------------------------------------------------
I9_2:
		IN	AL,61H			;These instructions
		MOV	AH,AL			; reset the keyboard
		OR	AL,80H			; controller
		OUT	61H,AL
		MOV	AL,AH
		JMP	SHORT $+2		;Flush the prefetch
		JMP	SHORT $+2		; queue for I/O delay
		OUT	61H,AL

		CLI				;Disable interrupts
		MOV	AL,20H			;Reset the controller
		OUT	20H,AL
		STI				;Enable interrupts
;----------------------------------------------------------------------
; If POPWATCH is already active, ignore the request.
;----------------------------------------------------------------------
		CMP	BYTE PTR CS:[ACTIVE],0	;If active, ignore
		JNE	I9_3

		MOV	BYTE PTR CS:[REQUEST],-1 ;Set pop up request
I9_3:
		POP	AX			;Restore register
		IRET				;Return from INT

INT_9		ENDP

;======================================================================
; INT_8 (ISR)
;
; This routine gets control each timer tick. If a pop up request is
; pending and DOS is not busy, it pops up the window. Interrupts are
; not enabled until popping up is certain.
;----------------------------------------------------------------------
INT_8		PROC	FAR
	ASSUME	CS:CSEG, DS:NOTHING, ES:NOTHING, SS:NOTHING
;----------------------------------------------------------------------
; If no request is pending, simply jump to the original interrupt.
;----------------------------------------------------------------------
		CMP	BYTE PTR CS:[REQUEST],0	;0 = no request
		JNE	I8_2
I8_1:
		DB	JMPFAR			;Jump to old interrupt
OLDINT8		DD	-1			; located here
;----------------------------------------------------------------------
; If already popped up, ignore the request.
;----------------------------------------------------------------------
I8_2:
		CMP	BYTE PTR CS:[ACTIVE],0	;Nonzero is active
		JNE	I8_1
;----------------------------------------------------------------------
; Determine if DOS is in a busy state by examining the DOS BUSY flag.
;----------------------------------------------------------------------
		PUSH	BX			;Save used registers
		PUSH	DS

		LDS	BX,CS:[DOS_BUSY]	;DS:BX -> DOS BUSY flag
	ASSUME	DS:NOTHING

		CMP	BYTE PTR DS:[BX],0	;Zero is not busy

		POP	DS			;Restore registers
	ASSUME	DS:NOTHING
		POP	BX
		JNE	I8_1
;----------------------------------------------------------------------
; It's okay for us to pop up. Cancel (acknowledge) the request. Set the
; active flag, then service the old Int 8.
;----------------------------------------------------------------------
		MOV	BYTE PTR CS:[REQUEST],0	;Cancel request
		MOV	BYTE PTR CS:[ACTIVE],-1	;Nonzero means busy

		STI				;Enable interrupts

		PUSHF				;Simulate interrupt
		CALL	DWORD PTR CS:[OLDINT8]	;Perform old Int 8

		JMP	POPUP			;Go pop up

INT_8		ENDP

;======================================================================
; INT_28 (ISR)
;
; Gets control each time DOS issues its idle interrupt. This proc
; checks to see if a pop up request is pending. Interrupts are not
; enabled until popping up is certain.
;----------------------------------------------------------------------
INT_28		PROC	FAR
	ASSUME	CS:CSEG, DS:NOTHING, ES:NOTHING, SS:NOTHING
;----------------------------------------------------------------------
; If no request is pending, simply jump to the original interrupt.
;----------------------------------------------------------------------
		CMP	BYTE PTR CS:[REQUEST],0	;0 = no request
		JNE	I28_2
I28_1:
		DB	JMPFAR			;Jmp to old interrupt
OLDINT28	DD	-1			; located here
;----------------------------------------------------------------------
; Cancel the request. If POPWATCH is already busy, we'll ignore it.
;----------------------------------------------------------------------
I28_2:
		MOV	BYTE PTR CS:[REQUEST],0	;Cancel request
		CMP	BYTE PTR CS:[ACTIVE],0	;Non-zero = active
		JNE	I28_1
;----------------------------------------------------------------------
; Set the active flag to indicate that we're popping up. Before we do,
; however, service the old Int 8.
;----------------------------------------------------------------------
		MOV	BYTE PTR CS:[ACTIVE],-1	;Nonzero means busy

		STI				;Enable interrupts
		PUSHF				;Simulate interrupt

		CALL	DWORD PTR CS:[OLDINT28]	;Perform old Int 28

		JMP	SHORT POPUP		;Go pop up

INT_28		ENDP

;======================================================================
; POPUP (Near)
;
; Control is tranferred to this routine to pop up the timer window on
; the screen. Other routines have determined that it is safe to do so.
;----------------------------------------------------------------------
; Entry: None
; Exit : None
;----------------------------------------------------------------------
; Changes: None (ISR protocols)
;----------------------------------------------------------------------
L1		DB	BOX_COL+2 ,BOX_ROW+1,"DATE",0
L2		DB	BOX_COL+2 ,BOX_ROW+2,"TIME",0
L4		DB	BOX_COL+2 ,BOX_ROW+4,"ELAPSED",0
L6		DB	BOX_COL+2 ,BOX_ROW+6,"PRINTER IS O"
PRN_STAT	DB	"FF",0
L7		DB	BOX_COL+2 ,BOX_ROW+7,"CONSOLE IS O"
CON_STAT	DB	"N ",0

L9		DB	BOX_COL+3 ,BOX_ROW+9 ,"GO   STOP   RESET",0
L10		DB	BOX_COL+3 ,BOX_ROW+10,"PRINTER   CONSOLE",0

MSG_TBL		DW	L1, L2, L4
PC_TBL		DW	L6, L7, L9, L10
MSG_QTY		EQU	($-MSG_TBL)/2

;----------------------------------------------------------------------
POPUP		PROC	NEAR
	ASSUME	CS:CSEG, DS:NOTHING, ES:NOTHING, SS:NOTHING

		PUSH	AX			;Save registers
		PUSH	BX
		PUSH	CX
		PUSH	DX
		PUSH	SI
		PUSH	DI
		PUSH	BP
		PUSH	DS
		PUSH	ES

		MOV	AX,CS			;Point DS and ES
		MOV	DS,AX			; to this segment
	ASSUME	DS:CSEG
		MOV	ES,AX
	ASSUME	ES:CSEG

		CLD				;String moves forward
;----------------------------------------------------------------------
; Check that we're in a text video mode. Determine our colors.
;----------------------------------------------------------------------
		MOV	CL,CO_ATTR		;Assume color window

		MOV	AH,0FH			;Get current video mode fn
		INT	10H			; thru BIOS

		CMP	AL,3			;All CGA text modes
		JBE	P_1

		MOV	CL,BW_ATTR		;Set mono colors
		CMP	AL,7			;Mono text mode?
		JE 	P_1
;----------------------------------------------------------------------
; Return to whoever issued the original Int 8 or Int 28h.
;----------------------------------------------------------------------
P_EXIT:
		POP	ES			;Restore all registers
	ASSUME	ES:NOTHING
		POP	DS
	ASSUME	DS:NOTHING
		POP	BP
		POP	DI
		POP	SI
		POP	DX
		POP	CX
		POP	BX
		POP	AX

		MOV	BYTE PTR CS:[ACTIVE],0	;Turn off active flag
		IRET
;----------------------------------------------------------------------
; Save the current video details for later restoration.
;----------------------------------------------------------------------
	ASSUME	DS:CSEG, ES:CSEG
P_1:
		MOV	[VATTR],CL		;Save color scheme
		MOV	[VPAGE],BH		;Save current page

		MOV	AH,3			;Get cursor position fn
		INT	10H			; thru BIOS

		MOV	[CUR_SIZ],CX		;Save cursor size
		MOV	[CUR_POS],DX		;Save cursor position
;----------------------------------------------------------------------
; This BIOS call makes the cursor invisible on most video adapters.
; Some EGAs, however, have trouble with it.
;----------------------------------------------------------------------
		MOV	AH,1			;Set cursor shape
		MOV	CX,2000H		;Invisible on most PCs
		INT	10H			; thru BIOS
;----------------------------------------------------------------------
; Save the section of the screen we will be writing over.
;----------------------------------------------------------------------
		MOV	SI,-1			;Tell proc to save
		MOV	DI,OFFSET SCREEN_BUF	;Destination for save
		CALL	SCREEN			;Save user's screen
;----------------------------------------------------------------------
; Clear the screen and draw the popup window and shadow.
;----------------------------------------------------------------------
		CALL	DRAW_BOX		;Create frame
;----------------------------------------------------------------------
; Fill in the text labels.
;----------------------------------------------------------------------
		MOV	BX,OFFSET MSG_TBL	;Message located here
		MOV	CX,MSG_QTY		;Display this many
		CALL	WR_STRINGS
;----------------------------------------------------------------------
; Display timer info and act on commands.
;----------------------------------------------------------------------
		CALL	CLOCK			;Activate clocks
;----------------------------------------------------------------------
; Restore the screen to its original state.
;----------------------------------------------------------------------
		MOV	SI,OFFSET SCREEN_BUF	;Point to data
		CALL	SCREEN			;Restore screen

		MOV	AH,1			;Set cursor shape
		MOV	CX,[CUR_SIZ]		;Restore original shape
		INT	10H			; thru BIOS

		MOV	AH,2			;Set Cursor position
		MOV	DX,[CUR_POS]		;Restore old cursor
		INT	10H			; thru BIOS

		JMP	P_EXIT

POPUP		ENDP

;======================================================================
; SCREEN (Near)
;
; Saves or restores the screen data in our window area based on the
; values passed to it. Uses BIOS functions to read and write screen.
;----------------------------------------------------------------------
; Entry:
;	SI = -1, copy from screen to buffer
;	     ES:DI = destination buffer address
;
;	SI != -1, copy from buffer at DS:SI to screen
; Exit:
;	None
;----------------------------------------------------------------------
; Changes: AX BX CX DX SI DI
;----------------------------------------------------------------------
SCREEN		PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:CSEG, SS:NOTHING

		MOV	BH,[VPAGE]		;Active video page
		MOV	CX,NROW+1		;Row loop counter
		MOV	DH,BOX_ROW		;Init row pointer
;----------------------------------------------------------------------
; Loop here for each row.
;----------------------------------------------------------------------
S_1:
		PUSH	CX			;Save row counter

		MOV	CX,NCOL+2		;Set up column loop
		MOV	DL,BOX_COL		;Column Pointer
;----------------------------------------------------------------------
; Loop here for each column.
;----------------------------------------------------------------------
S_2:
		PUSH	CX			;Save column counter

		MOV	AH,2			;Position cursor fn
		INT	10H			; thru BIOS

		CMP	SI,-1			;SI =FFFF if SAVE
		JE	S_3
;----------------------------------------------------------------------
; Restore the screen from the buffer at DS:SI.
;----------------------------------------------------------------------
		LODSW				;Get char and attribute
		MOV	BL,AH			;Put attribute where needed
		MOV	AH,9			;Write char fn
		MOV	CX,1			;Write one copy of char
		INT	10H			; thru BIOS
		JMP	SHORT S_4
;----------------------------------------------------------------------
; Copy from the screen to the buffer at ES:DI.
;----------------------------------------------------------------------
S_3:
		MOV	AH,8			;Get char & attribute fn
		INT	10H			; thru BIOS
		STOSW				;Write to buffer
;----------------------------------------------------------------------
; Loop for each column and row.
;----------------------------------------------------------------------
S_4:
		INC	DL			;Next column
		POP	CX			;Restore column counter
		LOOP	S_2			;Close Inner loop

		INC	DH			;Next row
		POP	CX			;Restore row counter
		LOOP	S_1			;Close Outer loop

		RET

SCREEN		ENDP

;======================================================================
; DRAW_BOX (Near)
;
; Clears a window (box) on the screen and builds the popup window.
;----------------------------------------------------------------------
; Entry: None
; Exit : None
;----------------------------------------------------------------------
; Changes: AX BX CX DX SI
;----------------------------------------------------------------------
BOX_MSG		DB	BOX_COL+3,BOX_ROW,181,"PopWatch  1.00",198,0
BOX_CHARS	DB	201,205,187	;Top row
		DB	186,196,186	;Middle rows
		DB	200,205,188	;Bottom row

;----------------------------------------------------------------------
DRAW_BOX	PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:CSEG, SS:NOTHING
;----------------------------------------------------------------------
; Clear two overlapping rectangles on the screen to create a shadow
; box effect.
;----------------------------------------------------------------------
		MOV	AX,0600H		;Scroll window fn
		MOV	CX,(BOX_ROW+1)*100H+BOX_COL+1
		MOV	DX,(BOX_ROW+NROW)*100H+BOX_COL+NCOL+1
		MOV	BH,[VATTR]		;Window color
		INT	10H			; thru BIOS

		MOV	AX,0600H		;Scroll window fn
		MOV	CX,BOX_ROW*100H+BOX_COL
		MOV	DX,(BOX_ROW+NROW-1)*100H+BOX_COL+NCOL-1
		MOV	BH,[VATTR]		;Window color
		INT	10H			; thru BIOS
;----------------------------------------------------------------------
; Draw box as one top line, repeated middle lines, and one end line.
;----------------------------------------------------------------------
		MOV	DH,CH			;Cur row from last call
		MOV	BH,[VPAGE]		;Active video page
		MOV	SI,OFFSET BOX_CHARS	;Chars to draw
		MOV	CX,NROW			;Number of rows to draw
CB_1:
		PUSH	CX			;Save counter

		MOV	AH,2			;Position cursor
		MOV	DL,BOX_COL		; to this row,column
		INT	10H			; thru BIOS

		LODSB				;Get leftmost char
		MOV	AH,0EH			;Write char TTY
		INT	10H			; thru BIOS

		LODSB				;Get middle char
		MOV	AH,0AH			;Write repeated char
		MOV	CX,NCOL-2		;This many copies
		INT	10H			; thru BIOS

		MOV	AH,2			;Position cursor
		MOV	DL,BOX_COL+NCOL-1	;Col = righthand edge
		INT	10H			; thru BIOS

		LODSB				;Get rightmost char
		MOV	AH,0EH			;Write char TTY
		INT	10H			; thru BIOS

		INC	DH			;Next row
		POP	CX			;Restore counter
;----------------------------------------------------------------------
; The first and last rows are drawn once. For the middle rows, SI is
; backed up and the chars are used again.
;----------------------------------------------------------------------
		CMP	CL,NROW			;Jump if 1st row
		JE	CB_2

		CMP	CL,2			;Jump if last row
		JE	CB_2

		SUB	SI,3			;Repeat row chars
CB_2:
		LOOP	CB_1
;----------------------------------------------------------------------
; Display the program's name in the box border.
;----------------------------------------------------------------------
		MOV	SI,OFFSET BOX_MSG	;Message to write
		CALL	WR_MESSAGE

		RET

DRAW_BOX	ENDP

;======================================================================
; WR_MESSAGE (Near)
;
; Write a string to the screen using the BIOS TTY function.
;----------------------------------------------------------------------
; Entry:
;	DS:SI = string to write
;	  String format is:
;	  DB BIOS screen column
;	  DB BIOS screen row
;	  DB text,0
; Exit:
;	None
;----------------------------------------------------------------------
; Changes: AX BX DX SI
;----------------------------------------------------------------------
WR_MESSAGE	PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:NOTHING, SS:NOTHING

		MOV	BH,[VPAGE]		;Get active page
		LODSW				;Get cursor position
		MOV	DX,AX			; in DX

		MOV	AH,2			;Position cursor fn
WM_1:
		INT	10H			; thru BIOS

		MOV	AH,0EH			;Write TTY
		LODSB				;Get character
		OR	AL,AL			;0 if end of string
		JNZ	WM_1

		RET

WR_MESSAGE	ENDP

;======================================================================
; WR_STRINGS (Near)
;
; Copy a list of strings to the console.
;----------------------------------------------------------------------
; Entry:
;	DS:BX = offset of table of near string pointers.
; Exit:
;	None
;----------------------------------------------------------------------
; Changes: BX CX SI
;----------------------------------------------------------------------
WR_STRINGS	PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:NOTHING, SS:NOTHING
WS_1:
		PUSH	CX			;Save string count

		MOV	SI,[BX]			;Get pointer
		INC	BX			;Point to next
		INC	BX			; pointer

		PUSH	BX			;Save across call
		CALL	WR_MESSAGE		;Write to screen
		POP	BX			;Restore pointer

		POP	CX			;Restore counter
		LOOP	WS_1

		RET

WR_STRINGS	ENDP

;======================================================================
; CLOCK (Near)
;
; Display the stopwatch interface on the screen.
;----------------------------------------------------------------------
; Entry: None
; Exit : None
;----------------------------------------------------------------------
; Changes: AX BX CX SI DI
;----------------------------------------------------------------------
CON_FLAG	DB	-1		;Console print, on
PRN_FLAG	DB	0		;Printer flag, off
REFRESH		DB	0		;Nonzero when refreshing window

REF_TICKS	DD	0		;32-bit elapsed tick reference

DATE_B		DB	BOX_COL+12,BOX_ROW+1,"MM/DD/YY",0 ;Date

TIME_B		DB	BOX_COL+12,BOX_ROW+2,"HH:MM:SS",0 ;Time
ET_B		DB	BOX_COL+12,BOX_ROW+4,"HH:MM:SS",0 ;Elapsed time

UPD_TBL		DW	TIME_B, ET_B
UPD_QTY		EQU	($-UPD_TBL)/2

;----------------------------------------------------------------------
CLOCK		PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:CSEG, SS:NOTHING
;----------------------------------------------------------------------
; Display the date once. This never changes.
;----------------------------------------------------------------------
		MOV	SI,OFFSET DATE_B	;Pointer to message
		CALL	WR_MESSAGE		;Write to CRT
;----------------------------------------------------------------------
; Begin the refresh cycle.
;----------------------------------------------------------------------
CL_1A:
		NOT	BYTE PTR [REFRESH]	;Non-zero = refresh
CL_1B:
		MOV	AH,1			;Check if key ready
		INT	16H			; Thru BIOS
		JNZ	CL_1C
;----------------------------------------------------------------------
; No key is waiting. Call the DO_CMD proc to write the new time and
; elapsed time values to the strings. The strings are then copied to
; the screen.
;----------------------------------------------------------------------
		MOV	DI,OFFSET TIME_B+2	;String destination
		MOV	AL,"t"			;Update time
		CALL	DO_CMD

		MOV	DI,OFFSET ET_B+2	;String destination
		MOV	AL,"e"			;Update elapsed time
		CALL	DO_CMD

		MOV	CX,UPD_QTY		;Number of strings
		MOV	BX,OFFSET UPD_TBL	;Pointer to pointers
		CALL	WR_STRINGS		;Write to screen
      		JMP	CL_1B
;----------------------------------------------------------------------
; The BIOS reports a key is ready. End refresh mode, fetch the key, and
; process it.
;----------------------------------------------------------------------
CL_1C:
		NOT	BYTE PTR [REFRESH]	;0 = Refresh complete

		SUB	AH,AH			;Get the key
		INT	16H			; thru BIOS
;----------------------------------------------------------------------
; Check the keystroke and act on it.
;----------------------------------------------------------------------
		CMP	AL,ESCAPE		;ESC means exit
		JE	CL_2A

		CMP	AX,ENDKEY		;END means exit
		JNE	CL_2B
CL_2A:
		RET
;----------------------------------------------------------------------
; Let DO_CMD determine if this is a valid command.
;----------------------------------------------------------------------
CL_2B:
		OR	AL,20H			;Make lower case
		SUB	AH,AH			;Indicate key input

		CALL	DO_CMD			;Act on the command
		JMP	CL_1A

CLOCK		ENDP

;======================================================================
; DO_CMD (Near)
;
; Interprets commands that are passed to it to turn on and off options
; and display information. This procedure acts somewhat differently
; when running in REFRESH mode.
;----------------------------------------------------------------------
; Entry:
;	ES:DI = string destination if refreshing
; Exit:
;	None
;
; Note:
;   When called from the resident portion, CS=DS=ES=resident segment.
;   When called from the currently executing copy CS=DS=seg of current
;     copy and ES=seg of resident copy.
;----------------------------------------------------------------------
; Changes: BX CX DX DI
;----------------------------------------------------------------------
STDOUT		EQU	1		;System file handles
STDPRN		EQU	4

ET_FLAG		DB	0		;Elapsed timer, defaults to off

TIME_MSG	DB	"PopWatch -> "
TIME_SLOT	DB	"XX:XX:XX",CR,LF
TIME_LEN	EQU	$-TIME_MSG

;----------------------------------------------------------------------
DO_CMD		PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:NOTHING, SS:NOTHING

		PUSH	AX			;Save registers
		PUSH	SI
;----------------------------------------------------------------------
; The P and C functions can be processed during both popup and command
; line execution.
;----------------------------------------------------------------------
		CMP	AL,"p"			;Printer status
		JE	DC_1A

		CMP	AL,"c"			;Console status
		JNE	DC_2
;----------------------------------------------------------------------
; C = change the console output status.
;----------------------------------------------------------------------
		MOV	BX,OFFSET CON_FLAG	;Point BX to flag
		MOV	DI,OFFSET CON_STAT	;Point DI to string
		JMP	SHORT DC_1B
;----------------------------------------------------------------------
; P = change the printer output status.
;----------------------------------------------------------------------
DC_1A:
		MOV	BX,OFFSET PRN_FLAG	;Point BX to flag
		MOV	DI,OFFSET PRN_STAT	;Point DI to string
;----------------------------------------------------------------------
; If a + or - was present, set the status to ON or OFF, respectively.
; If not, just toggle the current state.
;----------------------------------------------------------------------
DC_1B:
		MOV	AL,BYTE PTR ES:[BX]	;Get current flag
		NOT	AL			;Toggle it
		OR	AH,AH			;0 = toggle request
		JZ	DC_1C

		SUB	AL,AL			;Turn flag off
		CMP	AH,"-"			;Was that the request?
		JE	DC_1C

		NOT	AL			;Turn flag on
		CMP	AH,"-"			;Was that the request?
		JNE	DC_EXIT
DC_1C:
		MOV	BYTE PTR ES:[BX],AL	;Save new status

		OR	AL,AL			;Check final status
		MOV	AX,"FF"			;Change string to OFF
		JE	DC_1D

		MOV	AX," N"			; or to ON
DC_1D:
		STOSW				;Write word to ES:DI

		CMP	BYTE PTR ES:[ACTIVE],0	;If command line mode
		JE	DC_EXIT			; return
;----------------------------------------------------------------------
; Update the window in popup mode.
;----------------------------------------------------------------------
		MOV	BX,OFFSET PC_TBL	;Table of pointers
		MOV	CX,2			;P and C strings only
		CALL	WR_STRINGS		;Write to screen
		JMP	SHORT DC_EXIT
;----------------------------------------------------------------------
; Process the options related to the elapsed timer.
;----------------------------------------------------------------------
DC_2:
		MOV	BX,OFFSET REF_TICKS	;Point to reference
;----------------------------------------------------------------------
; R = reset the elapsed timer to 0 and start it.
;----------------------------------------------------------------------
		CMP	AL,"r"			;Reset timer?
		JNE	DC_3

		MOV	AH,-1			;Turn timer on
		CALL	GET_TICKS		;Get ticks in CX:DX
		JMP	SHORT DC_4C
;----------------------------------------------------------------------
; G = start the elapsed timer, retaining cumulative value.
;----------------------------------------------------------------------
DC_3:
		MOV	AH,ES:[ET_FLAG]		;Get timer status in AH

		CMP	AL,"g"			;G = turn timer on
		JNE	DC_4A

		OR	AH,AH			;Ignore if already on
		JNZ	DC_EXIT

		NOT	AH			;AH = FF
		JMP	SHORT DC_4B
;----------------------------------------------------------------------
; S = stop the timer at its current value.
;----------------------------------------------------------------------
DC_4A:
		CMP	AL,"s"			;Turn timer off?
		JNE	DC_5A

		OR	AH,AH			;Ignore if already off
		JZ	DC_EXIT

		SUB	AH,AH			;Turn timer off
;----------------------------------------------------------------------
; Get the current ticks. Calculate the difference between the reference
; and the current and store the result back into the reference.
;----------------------------------------------------------------------
DC_4B:
		CALL	GET_TICKS		;Get ticks in CX:DX

		SUB	DX,WORD PTR ES:[BX][0]	;Low word
		SBB	CX,WORD PTR ES:[BX][2]	;High word
DC_4C:
		MOV	WORD PTR ES:[BX][0],DX	;Save low word
		MOV	WORD PTR ES:[BX][2],CX	; and high word
DC_4D:
		MOV	ES:[ET_FLAG],AH		;Update timer status
;----------------------------------------------------------------------
; Exit the routine.
;----------------------------------------------------------------------
DC_EXIT:
		POP	SI			;Restore registers
		POP	AX
		RET				;Return from proc
;----------------------------------------------------------------------
; The D command can only be performed in command line mode.
; It is ignored when popped up and never called during refresh.
;----------------------------------------------------------------------
DC_5A:
		CMP	AL,"d"			;Report date
		JNE	DC_6A

		CMP	BYTE PTR ES:[ACTIVE],0	;Must be inactive
		JNE	DC_EXIT
;----------------------------------------------------------------------
; D = display the current date.
;----------------------------------------------------------------------
		MOV	SI,OFFSET DATE_B+2	;ES:SI = source
		MOV	DI,OFFSET TIME_SLOT	;DS:DI = destination
		MOV	CX,8			;String length
DC_5B:
		MOV	AL,ES:[SI]		;Get date digit
		MOV	DS:[DI],AL		;Put in local string
		INC	DI			;Advance pointers
		INC	SI
		LOOP	DC_5B
		JMP	SHORT DC_8B
;----------------------------------------------------------------------
; The T and E commands can be performed in command line mode or during
; a resident refresh, but not from the keyboard while popped up.
;----------------------------------------------------------------------
DC_6A:
		CMP	BYTE PTR ES:[ACTIVE],0	;Must be command line
		JE	DC_6B

		CMP	BYTE PTR ES:[REFRESH],0	; or refreshing
		JE	DC_EXIT
;----------------------------------------------------------------------
; E = report or refresh elapsed time.
;----------------------------------------------------------------------
DC_6B:
		CMP	AL,"e"			;Report elapsed time
		JNE	DC_7A
;----------------------------------------------------------------------
; If the elapsed timer is off, simply convert the stored number.
;----------------------------------------------------------------------
		CMP	BYTE PTR ES:[ET_FLAG],0	;Check timer status
		JNE	DC_6C

		MOV	DX,WORD PTR ES:[REF_TICKS][0]	;Low word
		MOV	CX,WORD PTR ES:[REF_TICKS][2]	;High word
		JMP	SHORT DC_7B
;----------------------------------------------------------------------
; If the timer is on, get the difference between the current time and
; the reference time.
;----------------------------------------------------------------------
DC_6C:
		CALL	GET_TICKS		;Get ticks in CX:DX

		SUB	DX,WORD PTR ES:[REF_TICKS][0]	;Low word
		SBB	CX,WORD PTR ES:[REF_TICKS][2]	;High word
		JMP	SHORT DC_7B
;----------------------------------------------------------------------
; T = report/refresh current time.
;----------------------------------------------------------------------
DC_7A:
		CMP	AL,"t"			;Report current time
		JNE	DC_EXIT

		CALL	GET_TICKS		;Get ticks in CX:DX
DC_7B:
		CALL	TICK2HMS		;Convert CX:DX to time
;----------------------------------------------------------------------
; If we're in refresh mode, put the string in the desired location.
;----------------------------------------------------------------------
		CMP	ES:[REFRESH],0		;Check refresh status
		JE	DC_8A

		MOV	CX,2			;Want HH:MM:SS
		CALL	ASC_TIME		;Perform the conversion
		JMP	DC_EXIT
;----------------------------------------------------------------------
; For command line mode, format the info into a displayable message.
;----------------------------------------------------------------------
DC_8A:
		PUSH	ES			;Save resident segment

		MOV	DI,OFFSET TIME_SLOT	;Point ES:DI to string
		PUSH	DS			;Assume nothing so
		POP	ES			; always consistent
	ASSUME	ES:NOTHING

		MOV	CX,2			;Want HH:MM:SS
		CALL	ASC_TIME		;Perform the conversion

		POP	ES			;Restore segment
	ASSUME	ES:NOTHING
;----------------------------------------------------------------------
; In command line mode, the display may be going to the console...
;----------------------------------------------------------------------
DC_8B:
		CMP	BYTE PTR ES:[CON_FLAG],0 ;0 = don't write
		JE	DC_8C

		MOV	AH,40H			;Write to handle
		MOV	BX,STDOUT		;Console
		MOV	CX,TIME_LEN		;This many bytes
		MOV	DX,OFFSET TIME_MSG	;From this location
		INT	21H			; thru DOS
;----------------------------------------------------------------------
; ...and it may go to the printer.
;----------------------------------------------------------------------
DC_8C:
		CMP	BYTE PTR ES:[PRN_FLAG],0 ;0 = don't write
		JE	DC_8D

		MOV	AH,40H			;Write to handle
		MOV	BX,STDPRN		;Printer
		MOV	CX,TIME_LEN		;This many bytes
		MOV	DX,OFFSET TIME_MSG	;From this location
		INT	21H			; thru DOS
DC_8D:
		JMP	DC_EXIT

DO_CMD		ENDP

;======================================================================
; GET_TICKS (Near)
;
; This routine performs the same function as the BIOS Get Tick Count
; (Int 1Ah, AH=0), but takes about half the time.
;----------------------------------------------------------------------
; Entry:
;	None
; Exit:
;	CX:DX = 32-bit timer tick clock count
;----------------------------------------------------------------------
; Changes: CX DX
;----------------------------------------------------------------------
TIMER_ADR	DD	0000046CH		;Address of clock count

;----------------------------------------------------------------------
GET_TICKS	PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:NOTHING, SS:NOTHING

		PUSH	BX			;Preserve registers
		PUSH	DS

		LDS	BX,[TIMER_ADR]		;Point to tick count
	ASSUME	DS:NOTHING

		LDS	DX,[BX]			;Get count in DS:DX
	ASSUME	DS:NOTHING

		MOV	CX,DS			;Put high count in CX

		POP	DS			;Restore registers
	ASSUME	DS:CSEG
		POP	BX
		RET

GET_TICKS	ENDP

;======================================================================
; TICK2HMS (Near)
;
; Convert BIOS timer ticks to HH:MM:SS.ss. The math in this routine
; was "borrowed" from the internal DOS routines that convert timer
; ticks to time representation.
;----------------------------------------------------------------------
; Entry:
;	CX:DX = 32-bit timer tick count
; Exit:
;	AH = hours
;	AL = minutes
;	BH = seconds
;	BL = hundredths of seconds
;----------------------------------------------------------------------
; Changes: AX BX
;----------------------------------------------------------------------
TICK2HMS	PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:NOTHING, SS:NOTHING

		PUSH	CX			;Save used registers
		PUSH	DX

		MOV	AX,CX
		MOV	BX,DX
		SHL	DX,1
		RCL	CX,1
		SHL	DX,1
		RCL	CX,1
		ADD	DX,BX
		ADC	AX,CX
		XCHG	AX,DX
		MOV	CX,0E90BH
		DIV	CX
		MOV	BX,AX
		XOR	AX,AX
		DIV	CX
		MOV	DX,BX
		MOV	CX,00C8H
		DIV	CX
		CMP	DL,64H
		JB	TICK1
		SUB	DL,64H
TICK1:
		CMC
		MOV	BL,DL
		RCL	AX,1
		MOV	DL,00
		RCL	DX,1
		MOV	CX,003CH
		DIV	CX
		MOV	BH,DL
		DIV	CL
		XCHG	AL,AH
		ADD	BL,5			;Correct for round off

		POP	DX			;Restore registers
		POP	CX
		RET

TICK2HMS	ENDP

;======================================================================
; ASC_TIME (Near)
;
; Converts HH:MM:SS.ss time in AX:BX registers to an ASCII string in
; HH:MM:SS.ss format. The precision of the conversion can be specified.
;----------------------------------------------------------------------
; Entry:
;	AH = hours
;	AL = minutes
;	BH = seconds
;	BL = hundredths
;	ES:DI = destination for ASCII string
;	CX = parsing
;	  0 - HH
;	  1 - HH:MM
;	  2 - HH:MM:SS
;	  3 - HH:MM:SS.ss
; Exit:
;	None
;----------------------------------------------------------------------
; Changes: CX DI
;----------------------------------------------------------------------
ASC_TIME	PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:NOTHING, SS:NOTHING

		PUSH	DX			;Preserve register
;----------------------------------------------------------------------
; Convert the time numbers to ASCII and store in the string at ES:DI.
;----------------------------------------------------------------------
		MOV	DL,AL			;Save MM
		MOV	DH,":"			;Save colon char

		MOV	AL,AH			;Get hours
		CALL	STO_ASC			;Convert it
		JCXZ	AT_EXIT

		DEC	CX			;Change parse flag
		MOV	AL,DH			;Write a colon
		STOSB				; to ES:DI

		MOV	AL,DL			;Get minutes
		CALL	STO_ASC			;Convert it
		JCXZ	AT_EXIT

		DEC	CX			;Change parse flag
		MOV	AL,DH			;Write a colon
		STOSB				; to ES:DI

		MOV	AL,BH			;Get seconds
		CALL	STO_ASC			;Convert it
		JCXZ	AT_EXIT

		MOV	AL,"."			;Write a decimal point
		STOSB				; to ES:DI

		MOV	AL,BL			;Get hundredths
		CALL	STO_ASC			;Convert it
AT_EXIT:
		POP	DX			;Restore register
		RET

ASC_TIME	ENDP

;======================================================================
; STO_ASC (Near)
;
; Convert a number in AL to two denary ASCII characters and write to
; ES:DI. The number must fall within the range 0-99.
;----------------------------------------------------------------------
; Entry:
;	AL = number to convert. 0-99 only.
; Exit:
;	None
;----------------------------------------------------------------------
; Changes: AX DI
;----------------------------------------------------------------------
STO_ASC		PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:NOTHING, SS:NOTHING

		AAM				;Split denary digits
		OR	AX,"00"			;Convert to ASCII
		XCHG	AL,AH			;Reverse digits
		STOSW				;Write them
		RET

STO_ASC		ENDP

;======================================================================
; Data here is allocated after the program loads into memory to save
; space in the COM file. The PC assembler variable is used to keep
; track of relative addresses.
;----------------------------------------------------------------------
PC		=	$			;Set imaginary counter

SCREEN_BUF	=	PC			;DB NROW*NCOL*2 DUP(?)
PC		=	PC+(NROW+1)*(NCOL+2)*2

LAST_BYTE	=	PC

;======================================================================
; MAIN (Near)
;
; Execution begins here when POPWATCH is run from the command line.
;----------------------------------------------------------------------
; Transient data.
;----------------------------------------------------------------------
RESIDENT	DB	-1		;Cleared to 0 if not resident
UMB_LINK	DB	-1		;Holds UMB link status

;----------------------------------------------------------------------
; Messages.
;----------------------------------------------------------------------
COPYRIGHT$	DB	CR,LF,"PopWatch 1.00 Copyright (c) 1992 ",254
		DB	" Robert L. Hummel",CR,LF,"PC Magazine "
		DB	"Assembly Language Lab Notes",LF,CR,LF,"$"

RESIDENT$	DB	LF,"PopWatch 1.00 Already Resident",CR,LF,LF
USAGE$		DB	"Interactive : Pop up with ALT-P",CR,LF
		DB	"Command Line: PopWatch [D][T][E][G][S][R]"
		DB	"[C[+|-]][P[+|-]]",CR,LF,LF
		DB	"where",CR,LF
		DB	"  D = display date",CR,LF
		DB	"  T = display time",CR,LF
		DB	"  E = display elapsed time",CR,LF
		DB	"  G = start elapsed timer",CR,LF
		DB	"  S = stop elapsed timer",CR,LF
		DB	"  R = reset, then start elapsed timer",CR,LF
		DB	"  C = toggle console output",CR,LF
		DB	"  P = toggle printer output",CR,LF,LF
		DB	"  + = force output on",CR,LF
		DB	"  - = force output off",CR,LF,"$"

;----------------------------------------------------------------------
MAIN		PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:CSEG, SS:CSEG

		CLD				;String moves forward
;----------------------------------------------------------------------
; Determine if a copy of POPWATCH is already resident in memory by
; searching for a duplicate of the copyright notice.
; Modify marker string to avoid false matches when searching memory.
;----------------------------------------------------------------------
		NOT	WORD PTR [RES_MARKER]	;Modify marker
;----------------------------------------------------------------------
; Get the DOS version. If ver 5 or later, save the current UMB state,
; then link them to search all memory.
;----------------------------------------------------------------------
		MOV	AH,30H			;Get DOS version in AX
		INT	21H			; thru DOS

		CMP	AL,5			;Dos 5 or later
		JB	M_1

		MOV	AX,5802H		;Get current UMB link
		INT	21H			; thru DOS
		JC	M_1

		MOV	[UMB_LINK],AL		;Save it

		MOV	AX,5803H		;Set UMB to
		MOV	BX,1			; linked in chain
		INT	21H			; thru DOS
M_1:
;----------------------------------------------------------------------
; Get the segment address of the first MCB in BX.
;----------------------------------------------------------------------
		MOV	AH,52H			;Get ES:BX -> IVARS
		INT	21H			; thru DOS
	ASSUME	ES:NOTHING

		MOV	BX,ES:[BX-2]		;Get first MCB
;----------------------------------------------------------------------
; Point ES to the segment in BX and look for the modified copyright.
;----------------------------------------------------------------------
		MOV	AX,DS			;Current seg in AX
M_2A:
		MOV	ES,BX			;Point ES to MCB
	ASSUME	ES:NOTHING
		INC	BX			;Point to block

		MOV	SI,OFFSET RES_MARKER	;Compare DS:SI
		LEA	DI,[SI+10H]		; to ES:DI

		MOV	CX,MARKER_LEN		;Compare full string
		REPE	CMPSB			;CMP DS:SI TO ES:DI
		JNE	M_2B

		CMP	AX,BX			;If not this copy, done
		JNE	M_2C
M_2B:
;----------------------------------------------------------------------
; No match. Move to the next memory block.
;----------------------------------------------------------------------
		ADD	BX,ES:[3]		;Add block length

		CMP	BYTE PTR ES:[0],"Z"	;This block the last?
		JNE	M_2A
;----------------------------------------------------------------------
; If we get here, no resident copy was found in all of memory. Point ES
; to this copy, but assume NOTHING to consistently address resident
; copy.
;----------------------------------------------------------------------
		MOV	BYTE PTR [RESIDENT],0	;Say none resident
		MOV	BX,AX			;Set ES to this seg
M_2C:
		MOV	ES,BX			;Set to memory block
	ASSUME	ES:NOTHING
;----------------------------------------------------------------------
; Restore the UMB link to its previous state.
;----------------------------------------------------------------------
		MOV	BL,CS:[UMB_LINK]	;Original link state
		CMP	BL,-1			;Was it recorded?
		JE	M_2D

		SUB	BH,BH			;Link in BX
		MOV	AX,5803			;Set UMB link
		INT	21H			; thru DOS
M_2D:
;----------------------------------------------------------------------
; Update the date buffer. The DOS Get Date function returns with
; CX=YEAR, DH=MONTH, DL=DAY.
;----------------------------------------------------------------------
		MOV	AH,2AH			;Get date
		INT	21H			; thru DOS

		SUB	CX,1900			;Want last 2 digits

		MOV	DI,OFFSET DATE_B+2	;Destination
		MOV	AL,DH
		CALL	STO_ASC			;Write month

		INC	DI
		MOV	AL,DL
		CALL	STO_ASC			;Write day

		INC	DI
		MOV	AL,CL
		CALL	STO_ASC			;Write year
;----------------------------------------------------------------------
; Read the command line and process any parameters in the order in
; which they appear.
;----------------------------------------------------------------------
		MOV	SI,81H			;Command line address
M_3A:
		LODSB				;Get character

		CMP	AL,SPACE		;Skip blanks
		JE	M_3A

		CMP	AL,0DH			;CR means end
		JE	M_3D

		MOV	AH,AL			;Save switch in AH
		LODSB				;Get next char

		CMP	AL,"+"			;+ means turn on
		JE	M_3B

		CMP	AL,"-"			;- means turn off
		JE	M_3B

		DEC	SI			;Backup character
		SUB	AL,AL			;Clear AL to 0
M_3B:
		XCHG	AH,AL			;Put switch in AL
		OR	AL,20H			;Make it lower case
		CALL	DO_CMD			;Act on the command
		JMP	M_3A
M_3D:
;----------------------------------------------------------------------
; If there was no resident copy, go resident.
;----------------------------------------------------------------------
		CMP	BYTE PTR [RESIDENT],0	;0 = not resident
		JE	M_5
;----------------------------------------------------------------------
; If no command line parameters were given, print the usage message.
; If command line parameters were present, simply terminate.
;----------------------------------------------------------------------
		MOV	DX,OFFSET RESIDENT$	;Just print usage

		CMP	BYTE PTR DS:[80H],0	;0 if no parameters
		JNE	M_4

		MOV	AH,9			;Display string fn
		INT	21H			; thru DOS
M_4:
		MOV	AH,4CH			;Terminate
		INT	21H			; thru DOS
;----------------------------------------------------------------------
; Get a pointer to the DOS Critical Flag, a one-byte location in low
; memory that is set when DOS is in an uninterruptable state. Location
; is returned in ES:BX.
;----------------------------------------------------------------------
M_5:
		MOV	AH,34H			;Get DOS BUSY flag ptr
		INT	21H			; thru DOS
	ASSUME	ES:NOTHING

		MOV	WORD PTR DOS_BUSY[0],BX	;offset
		MOV	WORD PTR DOS_BUSY[2],ES	;segment
;----------------------------------------------------------------------
; Hook Int 9 for the hot-key detection routine.
; Hook Int 8 and Int 28h to pop up.
;----------------------------------------------------------------------
		PUSH	DS			;Reset ES to point to same
		POP	ES			; segment as DS
	ASSUME	ES:CSEG

		MOV	AL,8			;Interrupt number
		MOV	DI,OFFSET OLDINT8	;Store vector here
		MOV	DX,OFFSET INT_8		;New interrupt procedure
		CALL	SET_INT			;Make change
	ASSUME	ES:NOTHING

		MOV	AL,9			;Interrupt number
		MOV	DI,OFFSET OLDINT9	;Store vector here
		MOV	DX,OFFSET INT_9		;New interrupt procedure
		CALL	SET_INT			;Make change
	ASSUME	ES:NOTHING

		MOV	AL,28H			;Interrupt number
		MOV	DI,OFFSET OLDINT28	;Store vector here
		MOV	DX,OFFSET INT_28	;New interrupt procedure
		CALL	SET_INT			;Make change
	ASSUME	ES:NOTHING
;----------------------------------------------------------------------
; Deallocate the copy of the environment loaded with the program.
;----------------------------------------------------------------------
		MOV	AX,WORD PTR DS:[2CH]	;Address of environment
		MOV	ES,AX			;In ES register
	ASSUME	ES:NOTHING

		MOV	AH,49H			;Release allocated memory
		INT	21H			; thru DOS
;----------------------------------------------------------------------
; Display copyright notice and usage message.
;----------------------------------------------------------------------
		MOV	AH,9			;Display string
		MOV	DX,OFFSET COPYRIGHT$	;Copyright notice
		INT	21H			; thru DOS

		MOV	AH,9			;Display string
		MOV	DX,OFFSET USAGE$	;Show usage
		INT	21H			; thru DOS

		MOV	DX,(OFFSET LAST_BYTE - CSEG + 15) SHR 4
		MOV	AH,31H			;Keep (TSR)
		INT	21H			; thru DOS

MAIN		ENDP

;======================================================================
; SET_INT (Near)
;
; Get, save, and set an interrupt vector.
;----------------------------------------------------------------------
; Entry:
;	AL = vector number.
;	ES:DI = destination for old address.
;	DS:DX = new interrupt address.
; Exit:
;	None
;----------------------------------------------------------------------
; Changes: AX BX ES
;----------------------------------------------------------------------
SET_INT		PROC	NEAR
	ASSUME	CS:CSEG, DS:CSEG, ES:CSEG, SS:CSEG

		PUSH	AX			;Save vector # in AL

		MOV	AH,35H			;Get Int vector
		INT	21H			; thru DOS
	ASSUME	ES:NOTHING

		MOV	[DI+0],BX		;Save address in ES:DI
		MOV	[DI+2],ES

		POP	AX			;Get AL back
		MOV	AH,25H			;Set Int vector
		INT	21H			; thru DOS

		RET

SET_INT		ENDP

CSEG		ENDS
		END	ENTPT
