'===========================================================================
' Subject: INSTALLATION UTILITY               Date: 01-29-93 (10:58)       
'  Author: Ethan Winer                        Code: QB, PDS                
'  Origin: sansbach@gpu.srv.ualberta.ca     Packet: MISC.ABC
'===========================================================================
'********** PC-SETUP.BAS - PC Magazine Install Utility

'Copyright (c) 1992 Ethan Winer

'Note: If you run this program in the QB editor the ExeName function will
'return the directory that QB.EXE was run from, which is not necessarily the
'current directory.  In that case you may have to enter the directory name
'where your various .ZIP files reside for PC-SETUP to find them.
'
'If you are using Crescent's P.D.Q. you must search for all each call to the
'Interrupt routine, and change it as shown in the accompanying comments.  You
'can then compile and link this program for the smallest size possible as
'follows:
'
'   bc pc-setup /o/s;
'   link /nod/noe/packc/far/ex _
'     pc-setup _noread _noval _noerror _cprint , , nul , [basic7] pdq;
'   exe2com pc-setup               (optional)
'   del pc-setup.exe               (optional)
'   ren pc-setup.com pc-setup.exe  (optional)


DEFINT A-Z

'---- BASIC SUB and FUNCTION procedures in this program file
'
DECLARE SUB CopyFile (Source$)
DECLARE SUB DrawBox (ULRow, ULCol, LRRow, LRCol, Style)
DECLARE SUB DrawScreen ()
DECLARE SUB Editor (Text$, Row, LeftCol, Length, KeyCode)
DECLARE SUB EarlyEnd ()
DECLARE SUB ErrorEnd (Message$)
DECLARE SUB MidCharS (Work$, Position, NewChar)
DECLARE SUB ReadNames (Spec$, Array$())
DECLARE SUB SelectFiles (FileNames$(), Choice, ExitCode)
DECLARE SUB SetDrive (Drive$)
DECLARE SUB StuffBuf (Work$)

DECLARE FUNCTION ChangeDir% (DirName$)
DECLARE FUNCTION DOSVersion% ()
DECLARE FUNCTION Execute% (FileName$, Parameter$)
DECLARE FUNCTION ExeName$ ()
DECLARE FUNCTION FileCount% (FileSpec$, DirFlag)
DECLARE FUNCTION GetComment$ (FileName$)
DECLARE FUNCTION GetDir$ ()
DECLARE FUNCTION GetDrive% ()
DECLARE FUNCTION IntVal% (Work$)
DECLARE FUNCTION MakeDir% (DirName$)
DECLARE FUNCTION MidChar% (Work$, Position)
DECLARE FUNCTION Prompt% (Which)
DECLARE FUNCTION SourceDir$ ()


'Define the TYPE and other shared variables needed for using CALL InterruptX.
'
TYPE RegType
  AX    AS INTEGER
  BX    AS INTEGER
  CX    AS INTEGER
  DX    AS INTEGER
  BP    AS INTEGER
  SI    AS INTEGER
  DI    AS INTEGER
  Flags AS INTEGER
  DS    AS INTEGER
  ES    AS INTEGER
END TYPE
DIM SHARED Regs AS RegType

TYPE DTAType                            'used by find first/next service
  Reserved  AS STRING * 21              'reserved for use by DOS
  Attribute AS STRING * 1               'the file's attribute
  FileTime  AS STRING * 2               'the file's time
  FileDate  AS STRING * 2               'the file's date
  FileSize  AS LONG                     'the file's size
  FileName  AS STRING * 13              'the file's name
END TYPE
DIM SHARED DTA AS DTAType

DIM SHARED DOS                          'so the DOS procedures can get at it
DIM SHARED ZBuffer AS STRING * 80       'holds ASCIIZ copies of DOS strings
DIM SHARED One, Zero, Zero$             'these save code when used in CALLs
DIM SHARED Temp, Temp$                  'these are reusable scratch variables

'---- Define some constants and variables, and colors based on display type.
'
CONST MaxFiles% = 19                    'max. number of .ZIP files per disk
CONST DirLength% = 25                   'length of dest. directory display

One = 1                                 'saves four bytes per use in a CALL
DOS = &H21                              'also saves four bytes per use
Zero$ = CHR$(0)                         'call CHR$() just once here
PadComment$ = SPACE$(36)                'holds each comment when printing
REDIM DirsMade$(1 TO 100)               'remembers directories we created

Bar$ = "Μ" + STRING$(78, 205) + "Ή"     'for the main screen separating bars
Msg$ = SPACE$(79)                       'for messages on the bottom line
IF INSTR(UCASE$(COMMAND$), "/B") THEN MonoFlag = -1   '/b forces mono colors

NormFG = 11: NormBG = 7                 'assume colors for a color display
HiFG = 11: HiBG = 4                     'menu and default directory colors
MainFG = 10: MainBG = 1                 'main screen and box FG and BG colors
CsrSize = 7                             'color displays use 8 scan lines

DEF SEG = 0                             'see if it's really a color display
MonoMon = (PEEK(&H463) = &HB4)          'if not, MonoMon now equals -1
IF MonoMon OR MonoFlag THEN             'it's monochrome or /b was used
  NormFG = 7: NormBG = 0
  HiFG = 15: HiBG = 0
  MainFG = 0: MainBG = 7
  IF MonoMon THEN CsrSize = 12          'mono displays use 13 scan lines
END IF

IF DOSVersion% < 300 THEN               'PC-SETUP requires DOS 3.0 or later
  PRINT "DOS 3.0 or later required."
  END
END IF

'---- Get the directory PC-SETUP was run from or prompt for it if needed, to
'     ensure that there's at least one .ZIP file present to install.
'
InstPath$ = SourceDir$
DO
  IF RIGHT$(InstPath$, 1) <> "\" THEN InstPath$ = InstPath$ + "\"
  InstSpec$ = InstPath$ + "*.ZIP"
  NumFiles = FileCount%(InstSpec$, Zero)
  IF NumFiles THEN EXIT DO
  PRINT "No .ZIP files were found."
  INPUT "Enter the source directory or press Enter to end: ", InstPath$
  IF LEN(InstPath$) = 0 THEN END
LOOP

IF FileCount%(InstPath$ + "PKUNZIP.EXE", Zero) = 0 THEN 'confirm PKUNZIP
  PRINT "Can't find PKUNZIP."                           '  is available
  END
END IF
     
IF MidChar%(InstPath$, 2) <> 58 THEN    'if there's no drive letter (:)
  InstPath$ = CHR$(GetDrive%) + ":" + InstPath$ 'append the current drive
END IF


'---- See if they're installing more than one disk, and if so how many.
'
NumDisks = 1                                'assume only one disk for now
NumDisksFile$ = InstPath$ + "NUMDISKS.*"    'concatenate these just once

IF FileCount%(NumDisksFile$, Zero) THEN
  DIM NumDisks$(1 TO 1)
  CALL ReadNames(NumDisksFile$, NumDisks$())
  Temp = INSTR(NumDisks$(1), ".")
  NumDisks = IntVal%(MID$(NumDisks$(1), Temp + 1))
END IF


'---- See if there's a DEFAULT.DIR file in the root directory of the first
'     distribution disk, and if so read its contents.  Here we're using
'     FileCount to merely see if the file exists.  If there's no DEFAULT.DIR
'     file, default to current drive and directory.  And if the current drive
'     is A or B replace that with C.
'
SaveDir$ = CHR$(GetDrive%) + ":" + GetDir$      'save this while we have it
DefaultDir$ = SaveDir$                          'now assign it as the default

Temp = ASC(DefaultDir$)                         'avoid using ASC() twice
IF Temp = 65 OR Temp = 66 THEN                  'don't default to A: or B:
  CALL MidCharS(DefaultDir$, 1, 67)             'if A: or B:, substitute C:
END IF

DefaultDirFile$ = InstPath$ + "DEFAULT.DIR"     'concatenate these just once

IF FileCount%(DefaultDirFile$, Zero) THEN       'open the file if it exists
  OPEN DefaultDirFile$ FOR INPUT AS #1
  INPUT #1, DefaultDir$                         'read the default directory,
  CLOSE                                         ' trim and capitalize (UCASE$
  DefaultDir$ = UCASE$(RTRIM$(LTRIM$(DefaultDir$))) ' is for cosmetics only)
END IF


'---- See if there's a PROGRAM.RUN file in the root directory of the first
'     distribution disk, and if so read its contents.
'
RunFileName$ = InstPath$ + "PROGRAM.RUN"        'concatenate these just once
IF FileCount%(RunFileName$, Zero) THEN          'open the file if it exists
  OPEN RunFileName$ FOR INPUT AS #1
  INPUT #1, RunName$
  CLOSE
  RunName$ = RunName$ + CHR$(13)                'simulate pressing Enter
  IF LEN(RunName$) > 15 THEN RunName$ = ""      'don't use name if too long
END IF


'---- This is the main installation loop that cycles through each diskette.
'
FOR Disk = 1 TO NumDisks

  '---- See how many .ZIP files there are on the current disk, and limit the
  '     number we'll handle to MaxFiles% if there are more than that.  Then
  '     draw/redraw the main screen.
  '
  NumFiles = FileCount%(InstSpec$, Zero)
  IF NumFiles > MaxFiles% THEN NumFiles = MaxFiles%
 
  CALL DrawScreen
 
  '---- Read the .ZIP file names and display them in a vertical menu.  Then
  '     read any default directories (if present) within each .ZIP file's
  '     comment, and display them in the destination directory fields.  For
  '     each .ZIP file that doesn't have a corresponding default directory
  '     in the comment, use the contents of the main DEFAULT.DIR file found
  '     in the root directory of the disk.
  '
  REDIM ZIPName$(1 TO NumFiles)
  REDIM DestDir$(1 TO NumFiles)
  REDIM Comment$(1 TO NumFiles)

  CALL ReadNames(InstSpec$, ZIPName$())

  FOR X = 1 TO NumFiles                 'look at each .ZIP file comment
    DestDir$(X) = SPACE$(DirLength%)    'create a string to hold the dest dir
    LSET DestDir$(X) = DefaultDir$      'assume none, use the global default
    Comment$(X) = GetComment$(InstPath$ + ZIPName$(X))
    Temp = INSTR(Comment$(X), "ώ")      'see if a directory was given
    IF Temp THEN                        'there is a directory for this file
      LSET DestDir$(X) = UCASE$(MID$(Comment$(X), Temp + 1)) 'dir is on right
      Comment$(X) = LEFT$(Comment$(X), Temp - 1)         'and comment on left
    END IF
  NEXT

  FOR X = 1 TO NumFiles                 'add leading blanks to make room
    ZIPName$(X) = "   " + ZIPName$(X)   '  for the CHR$(251) check marks
    IF RIGHT$(Comment$(X), 1) = "ϋ" THEN    'they want this file checked
      CALL MidCharS(ZIPName$(X), 2, 251)    'so check it in the file list box
      Comment$(X) = LEFT$(Comment$(X), LEN(Comment$(X)) - 1)
    END IF
    LSET PadComment$ = Comment$(X)      'display the directories and comments
    COLOR NormFG, NormBG                'while we're here
    LOCATE X + 4, 2: PRINT DestDir$(X);
    LOCATE X + 4, 44: PRINT PadComment$;
  NEXT

  DO                                    'let the user select the files
    CALL SelectFiles(ZIPName$(), Choice, ExitCode)
    IF ExitCode = 9 THEN                'they pressed Tab
      COLOR MainFG, MainBG
      LOCATE 25, 2
      PRINT SPC(30); "Tab: Select .ZIP files    F2: Begin    Esc: Quit";

      DO
        CALL Editor(DestDir$(Choice), Choice + 4, 2, 25, ExitCode)
        SELECT CASE ExitCode            'how did the terminate editing?
          CASE -80                      'Down Arrow
            Choice = Choice + 1         'wrap around if they go past the end
            IF Choice > NumFiles THEN Choice = 1
          CASE -72                      'Up Arrow
            Choice = Choice - 1         'wrap to the end if they go before 1
            IF Choice < 1 THEN Choice = NumFiles
          CASE 27                       'Escape
            CALL EarlyEnd
          CASE -60                      'F2
            ExitCode = -60              'tell SelectFiles to come right back
            EXIT DO                     '  so we can exit both levels of DO
          CASE ELSE
            EXIT DO                     'anything else returns to SelectFiles
        END SELECT
      LOOP
    ELSEIF ExitCode = -60 THEN          'F2
      ExitCode = 0                      'prevent unwanted recursion across
      EXIT DO                           '  multiple disks
    ELSEIF ExitCode = 27 THEN           'Escape
      CALL EarlyEnd
    END IF
  LOOP


  '---- Install the selected files to the specified destination directories.
  '     For each file that is tagged, either change to the appropriate drive
  '     and directory, or ensure that we're back to the original path.
  '
  FOR X = 1 TO NumFiles                       'for each .ZIP file present

    IF MidChar%(ZIPName$(X), 2) = 251 THEN    'if it's tagged to install

      IF X = 1 THEN RunProg = -1              'use PROGRAM.RUN only if first
                                              '  file is being installed
      COLOR MainFG, MainBG                    'for the status message below
      CLS
      LSET Msg$ = "Installing" + RTRIM$(MID$(ZIPName$(X), 3)) + "..."
      COLOR HiFG
      PRINT Msg$                              'advise the user as to progress
      COLOR MainFG
     
      DestPath$ = RTRIM$(DestDir$(X))         'work with a copy of the path

      IF MidChar%(DestPath$, 2) = 58 THEN     'if a drive was used (58 = ":")
        CALL SetDrive(DestPath$)              'change to that drive
        IF GetDrive% <> ASC(DestPath$) THEN   'no such drive
          CALL ErrorEnd("Drive " + LEFT$(DestPath$, 2) + " invalid")
        END IF
      ELSE
        CALL SetDrive(DefaultDir$)            'else switch to default drive
      END IF

      'strip off possible trailing "\" unless it refers to the root directory
      IF RIGHT$(DestPath$, 1) = "\" THEN
        Temp = LEN(DestPath$)
        IF Temp > 1 AND RIGHT$(DestPath$, 2) <> ":\" THEN
          DestPath$ = LEFT$(DestPath$, Temp - 1)
        END IF
      END IF

      Temp = -1                               'assume directory now exists
      IF LEN(DestPath$) THEN                  'if a directory name was given

        IF RIGHT$(DestPath$, 1) <> "\" THEN   'and it's not a root directory

          IF FileCount%(DestPath$, -1) = 0 THEN   'does the directory exist?
            IF MakeDir%(DestPath$) THEN       'no, so first try to create it
              CALL ErrorEnd("Cannot create " + DestPath$)
            END IF
            Temp = 0                          'it can't possibly have files
            DirsWeMade = DirsWeMade + 1       'show we created another one
            DirsMade$(DirsWeMade) = DestPath$ 'and remember its name
          END IF

          FOR Y = 1 TO DirsWeMade             'see if we made this directory
            IF DestPath$ = DirsMade$(Y) THEN  'yes, so there's no need to
              Temp = 0                        ' warn about overwriting files
              EXIT FOR
            END IF
          NEXT

        END IF

        IF ChangeDir%(DestPath$) THEN         'then try to change to it
          CALL ErrorEnd("Unable to access " + DestPath$)
        END IF

      END IF

      PKCmd$ = "-o "                          'assume they want to be warned
      IF Temp THEN                            'this directory existed
        IF Prompt%(Zero) THEN                 'ask if they want to be warned
          PKCmd$ = ""                         ' to overwrite existing files
        END IF                                ' without further prompting and
      END IF                                  ' use appropriate command if so

      IF NOT PKCopied THEN                    'copy PKUNZIP first time only
        PKCopied = -1                         'flag that we did it already
        CALL CopyFile(InstPath$)              'show where PKUNZIP.EXE is
        PKDir$ = RTRIM$(DestDir$(X))          'remember where we put it!
        IF LEN(PKDir$) = 0 THEN PKDir$ = DefaultDir$ 'use default dir if none
       
        Temp = ASC(RIGHT$(PKDir$, 1))         'check the right-most character
        IF Temp <> 58 AND Temp <> 92 THEN     'if not a colon or backslash
          PKDir2$ = "\"                       ' create a trailing "\"
        END IF                                ' which is appended below
      END IF

      '---- Install all of the files contained in this .ZIP file and check
      '     for an errors returned by either DOS or PKUNZIP.  Execute returns
      '     positive error values if PKUNZIP was run okay but it returned an
      '     error via the DOS Errorlevel.  If DOS itself reports an error
      '     (perhaps there wasn't enough memory to run the program) Execute
      '     returns the DOS error value as a negative number.  Error 8 is the
      '     DOS "Out of memory" error.
      '
      Temp = Execute%(PKDir$ + PKDir2$ + "PKUNZIP.EXE", PKCmd$ + InstPath$ + RTRIM$(MID$(ZIPName$(X), 4)))
      IF Temp THEN
        Temp$ = "PKUNZIP reports Error" + STR$(Temp)
        IF Temp < 0 THEN Temp$ = "Out of memory"
        CALL ErrorEnd(Temp$)
      END IF

      IF INKEY$ = CHR$(27) THEN         'allow aborting by pressing Escape
        CALL EarlyEnd
      END IF

    END IF

  NEXT

  IF Disk < NumDisks THEN               'if there are more disks to install
    LOCATE 25, 2                        'prompt to insert the next disk
    LSET Msg$ = "Insert the next disk and press any key when ready"
    COLOR MainFG, MainBG
    PRINT Msg$;
    LOCATE , 52
    DO: LOOP WHILE LEN(INKEY$)          'first clear any pending keys
    DO: LOOP UNTIL LEN(INKEY$)          'then wait for a keypress
    CALL SetDrive(SaveDir$)             'return to the current drive and its
    Temp = ChangeDir%(SaveDir$)         '  current directory before going on
  END IF

NEXT Disk


'---- Report success and run the specified program (StuffBuf ignores a null
'     string argument.
'
COLOR 7, 0: CLS
LOCATE 13, 30: PRINT "Installation complete!"
LOCATE 22, 1, 1
CALL SetDrive(PKDir$)                   'change to the first drive and
Temp = ChangeDir%(PKDir$)               ' directory we installed to
KILL "PKUNZIP.EXE"                      'delete the copy of PKUNZIP.EXE there
IF RunProg THEN CALL StuffBuf(RunName$) 'stuff the buffer if appropriate
END                                     'and end

FUNCTION ChangeDir% (DirName$)          'returns 0 if Okay, -1 if an error
 
  ZBuffer$ = DirName$ + Zero$           'make an ASCIIZ string
  Regs.AX = &H3B00                      'DOS change directory service
  Regs.DX = VARPTR(ZBuffer$)            'show DOS where ZBuffer$ is
  CALL Interrupt(DOS, Regs, Regs)       'call DOS
 'CALL Interrupt(DOS, Regs)             'use this with P.D.Q.

  IF Regs.Flags AND 1 THEN              'must be an invalid path
    ChangeDir% = -1                     'return -1 as an error
  END IF

END FUNCTION

SUB CopyFile (Source$) STATIC           'copies PKUNZIP.EXE
 
  Temp$ = Source$ + "PKUNZIP.EXE"
 
  IF FileCount%(Temp$, Zero) THEN
    OPEN Temp$ FOR BINARY AS #1         'open the input file if it exists
  ELSE                                  'if we can't find it, bag out with
    CALL ErrorEnd("Can't find PKUNZIP.EXE")     ' an error message
  END IF

  OPEN "PKUNZIP.EXE" FOR BINARY AS #2   'now open the target file

  Temp$ = SPACE$(LOF(1))                'make a buffer to hold PKUNZIP.EXE
  GET #1, , Temp$                       'read the source file
  PUT #2, , Temp$                       'write it to the destination
 
  CLOSE                                 'all done here
   
END SUB

FUNCTION DOSVersion% STATIC         'returns DOS version * 100 (3.30 = 330)

  Regs.AX = &H3000                  'DOS get DOS version service
  CALL Interrupt(DOS, Regs, Regs)
 'CALL Interrupt(DOS, Regs)         'use this with P.D.Q.

  'combine the major version in AL and the minor in AH
  DOSVersion% = (Regs.AX AND 255) * 100 + (Regs.AX \ 256)

END FUNCTION

SUB DrawBox (ULRow, ULCol, LRRow, LRCol, Style) STATIC
 
  Length = LRCol - ULCol + 1            'calculate this just once
 
  IF Style = 1 THEN
    LineType = 196
    VertBar$ = "³"
  ELSE
    LineType = 205
    VertBar$ = "Ί"
  END IF

  FOR X = ULRow TO LRRow                'first draw the walls
    LOCATE X, ULCol
    Temp = 32
    IF X = ULRow OR X = LRRow THEN Temp = LineType
    PRINT VertBar$; STRING$(Length - 2, Temp); VertBar$;
  NEXT

  IF Style = 1 THEN                     'then draw the corners
    LOCATE ULRow, ULCol: PRINT "Ϊ";
    LOCATE ULRow, LRCol: PRINT "Ώ";
    LOCATE LRRow, ULCol: PRINT "ΐ";
    LOCATE LRRow, LRCol: PRINT "Ω";
  ELSE
    LOCATE ULRow, ULCol: PRINT "Ι";
    LOCATE ULRow, LRCol: PRINT "»";
    LOCATE LRRow, ULCol: PRINT "Θ";
    LOCATE LRRow, LRCol: PRINT "Ό";
  END IF

END SUB

SUB DrawScreen STATIC

  SHARED MainFG, MainBG, Bar$
 
  '---- Draw the title screen and surrounding boxes.
  '
  COLOR MainFG, MainBG: CLS : LOCATE , , 0
  CALL DrawBox(One, One, 24, 80, 2)
  LOCATE 2, 24: PRINT "PC Magazine's PC-SETUP Version 1.00"
  LOCATE 3, 1: PRINT Bar$
 
  CALL DrawBox(3, 27, 24, 43, One)
  LOCATE 3, 27:  PRINT "ΡΝΝΝΝΝΝΝΝΝΝΝΝΝΝΝΡ";
  LOCATE 24, 27: PRINT "ΟΝΝΝΝΝΝΝΝΝΝΝΝΝΝΝΟ";
 
  LOCATE 4, 3: PRINT "Destination Directories";
  LOCATE , 31: PRINT "ZIP Files";
  LOCATE , 58: PRINT "Comments"

  LOCATE 25, 2
  PRINT "Up/Down/Space: Select files    Tab: Edit destination    F2: Begin    Esc: Quit";
 
END SUB

SUB EarlyEnd STATIC

  IF Prompt%(One) THEN
    COLOR 7, 0
    CLS
    LOCATE 24, , 1
    END
  END IF
  LOCATE , , 0

END SUB

SUB Editor (Text$, Row, LeftCol, Length, KeyCode) STATIC

  SHARED HiFG, HiBG, NormFG, NormBG, MonoMon, CsrSize

  '----- Work with a temporary copy.
  Edit$ = SPACE$(Length)
  LSET Edit$ = Text$

  '----- See where to begin editing and print the string.
  TxtPos = 1
  LOCATE Row, LeftCol, 1, CsrSize - 1, CsrSize
  COLOR HiFG, HiBG
  PRINT Edit$;

  '----- This is the main loop for handling key presses.
  DO
     LOCATE , LeftCol + TxtPos - 1, 1

     DO
       Ky$ = UCASE$(INKEY$)
     LOOP UNTIL LEN(Ky$)        'wait for a keypress

     IF LEN(Ky$) = 1 THEN       'create a key code
       KeyCode = ASC(Ky$)       'regular character key
     ELSE                       'extended key
       KeyCode = -ASC(RIGHT$(Ky$, 1))
     END IF

     '----- Branch according to the key pressed.
     SELECT CASE KeyCode

       '----- Backspace: decrement the pointer and the
       '      cursor, and ignore if in the first column.
       CASE 8
         TxtPos = TxtPos - 1
         IF TxtPos < 1 THEN TxtPos = 1
         LOCATE , LeftCol + TxtPos - 1, 0
         IF TxtPos > 0 THEN
           IF InsStatus THEN
             MID$(Edit$, TxtPos) = MID$(Edit$, TxtPos + 1) + " "
           ELSE
             MID$(Edit$, TxtPos) = " "
           END IF
           PRINT MID$(Edit$, TxtPos);
         END IF

       '----- Enter or Escape: this block is optional in
       '      case you want to handle these separately.
       CASE 13, 27
         EXIT DO                'exit the subprogram

       '----- Letter keys: turn off the cursor to hide
       '      the printing, handle Insert mode as needed.
       CASE 32 TO 254
         LOCATE , , 0
         IF InsStatus THEN      'expand the string
           MID$(Edit$, TxtPos) = Ky$ + MID$(Edit$, TxtPos)
           PRINT MID$(Edit$, TxtPos);
         ELSE                   'else insert character
           MID$(Edit$, TxtPos) = Ky$
           PRINT Ky$;
         END IF
         TxtPos = TxtPos + 1    'update position counter
         IF TxtPos > Length THEN TxtPos = Length

       '----- Left arrow: decrement the position counter.
       CASE -75
         TxtPos = TxtPos - 1
         IF TxtPos < 1 THEN TxtPos = 1

       '----- Right arrow: increment position counter.
       CASE -77
         TxtPos = TxtPos + 1
         IF TxtPos > Length THEN TxtPos = Length

       '----- Home: jump to the first character position.
       CASE -71
         TxtPos = 1

       '----- End: search for the last non-blank, and
       '      make that the current editing position.
       CASE -79
         FOR N = Length TO 1 STEP -1
           IF MID$(Edit$, N, 1) <> " " THEN EXIT FOR
         NEXT
         TxtPos = N + 1
         IF TxtPos > Length THEN TxtPos = Length

       '----- Insert key: toggle the Insert state and
       '      adjust the cursor size.
       CASE -82
         InsStatus = NOT InsStatus
         IF InsStatus THEN
           LOCATE , , , CsrSize \ 2, CsrSize
         ELSE
           LOCATE , , , CsrSize - 1, CsrSize
         END IF

       '----- Delete: delete the current character and
       '      reprint what remains in the string.
       CASE -83
         MID$(Edit$, TxtPos) = MID$(Edit$, TxtPos + 1) + " "
         LOCATE , , 0
         PRINT MID$(Edit$, TxtPos);

       '---- All other keys: exit the subprogram
       CASE ELSE
         EXIT DO
     END SELECT

  '----- Loop until the cursor moves out of the field.
  LOOP

  LSET Edit$ = LTRIM$(Edit$)    'trim and reprint the text in the normal
  LOCATE , 2                    ' color before returning
  COLOR NormFG, NormBG
  PRINT Edit$

  Text$ = RTRIM$(Edit$)         'now trim what's on the right too
 
END SUB

SUB ErrorEnd (Message$) STATIC

  COLOR 7, 0
  CLS
  LOCATE 13, 34 - LEN(Message$) \ 2, 1
  PRINT "Error: "; Message$; ", ending."
  END

END SUB

FUNCTION Execute% (Program$, Parameter$) STATIC
 
  '---- Prepare the program name and parameter strings for processing.  DOS
  '     requires that the parameter string hold the length of the parameter
  '     text, followed by the parameter text, and then followed by a CHR$(13)
  '     Enter byte.  The parameter block holds two CHR$(0) bytes followed by
  '     the address and segment of the parameter string.
  '
  DIM Block AS STRING * 14         'this is the DOS parameter block
  DIM Parm AS STRING * 50          'and this is the actual parameter text
 
  ZBuffer$ = Program$ + Zero$      'make an ASCIIZ string for DOS
 
  LSET Parm$ = CHR$(LEN(Parameter$)) + Parameter$ + CHR$(13)
  LSET Block$ = Zero$ + Zero$ + MKI$(VARPTR(Parm$)) + MKI$(VARSEG(Parm$))

  Dummy& = SETMEM(-500000)         'free up memory for PKUNZIP to run

  Regs.AX = &H4B00                 'DOS load/execute function
  Regs.DX = VARPTR(ZBuffer$)       'offset of program name into DX
  Regs.ES = VARSEG(Block$)         'segment of parameter block into ES
  Regs.BX = VARPTR(Block$)         'offset of parameter block into BX
  Regs.DS = -1                     'set DS to BASIC's segment
  CALL InterruptX(DOS, Regs, Regs) 'execute it as subordinate process
 'CALL InterruptX(DOS, Regs)       'use this with P.D.Q.

  IF Regs.Flags AND 1 THEN         'DOS had an error trying to run PKUNZIP
    Execute% = -Regs.AX            'set function value to exit code
    EXIT FUNCTION
  END IF

  Regs.AX = &H4D00                 'retrieve subordinate process code
  CALL Interrupt(DOS, Regs, Regs)
 'CALL Interrupt(DOS, Regs)        'use this with P.D.Q.
  Execute% = Regs.AX               'set function value to exit code

  Dummy& = SETMEM(500000)          'reclaim the memory reliquished eariler

END FUNCTION

FUNCTION ExeName$ STATIC

  'Returns the name of the currently running program; requires DOS 3.0 +

  '---- DOS Interrupt &H21 service &H62 returns the PSP segment in BX
  Regs.AX = &H6200
  CALL Interrupt(DOS, Regs, Regs)
 'CALL Interrupt(DOS, Regs)             'use this with P.D.Q.

  '---- The environment segment is at address &H2C/&H2D in PSP segment
  DEF SEG = Regs.BX
  DEF SEG = PEEK(&H2C) + PEEK(&H2D) * 256

  '---- Search the environment segment for two zero bytes in a row.  A count
  '     word (which we skip over) follows that, and the program name follows
  '     the count word.
  Byte = 0
  DO
    IF PEEK(Byte) = 0 THEN              'this is zero
      IF PEEK(Byte + 1) = 0 THEN        'this is too
        Byte = Byte + 2                 'so skip both
        EXIT DO                         'all done
      END IF
    END IF                              'else,
    Byte = Byte + 1                     'keep looking
  LOOP

  IF PEEK(Byte) = 1 THEN                'if this count byte = 1
    Byte = Byte + 2                     'the name follows
    DO WHILE PEEK(Byte)                 'up to another zero
      Tmp$ = Tmp$ + CHR$(PEEK(Byte))    'this is a different Tmp$ on purpose
      Byte = Byte + 1
    LOOP
    ExeName$ = Tmp$                     'assign the function output
  END IF

END FUNCTION

FUNCTION FileCount% (FileSpec$, DirFlag)
 
  Regs.DX = VARPTR(DTA)                 'set new DTA address
  Regs.AX = &H1A00                      'specify service 1Ah
  CALL Interrupt(DOS, Regs, Regs)       'DOS set DTA service
 'CALL Interrupt(DOS, Regs)             'use this with P.D.Q.

  Temp = 0                              'clear the counter
  ZBuffer$ = FileSpec$ + Zero$          'make an ASCIIZ string

  Regs.DX = VARPTR(ZBuffer$)            'the file spec address
  Regs.CX = 39                          'file attribute = all files
  IF DirFlag THEN Regs.CX = 39 OR 16    'include directories too
  Regs.AX = &H4E00                      'find first matching name service

  DO
    CALL Interrupt(DOS, Regs, Regs)     'see if there's a match
   'CALL Interrupt(DOS, Regs)           'use this with P.D.Q.
    IF Regs.Flags AND 1 THEN EXIT DO    'no more files

    IF DirFlag THEN                     'do we want directories?
      IF ASC(DTA.Attribute) AND 16 THEN 'yes, but is this a directory?
        IF ASC(DTA.FileName) <> 46 THEN 'filter "." and ".." (46 = period)
          Temp = Temp + 1               'we got another directory name
        END IF
      END IF
    ELSE
      Temp = Temp + 1                   'we got another file name
    END IF
     
    Regs.AX = &H4F00                    'find next name service
  LOOP

  FileCount% = Temp                     'assign the function output
 
END FUNCTION

FUNCTION GetComment$ (Zip$) STATIC      'read comment from file named in Zip$

  ZipID$ = "PK" + CHR$(5) + CHR$(6)     'this identifies a file as a ZIP file
 
  OPEN RTRIM$(Zip$) FOR BINARY AS #1    'open the .ZIP file
  FileSize& = LOF(1)                    'get and save its length
  BufferSize = 3072                     'the default header size
  IF BufferSize > FileSize& THEN BufferSize = FileSize&
  Temp$ = SPACE$(BufferSize)            'make buffer to receive ZIP header

  GET #1, FileSize& - BufferSize + 1, Temp$
  CLOSE

  Temp = 0                              'find the last occurrence of PK ID
  DO
     HeaderOffset = Temp                'remember where this one is
     Temp = INSTR(Temp + 1, Temp$, ZipID$)      'find the next one
  LOOP WHILE Temp                               'until no more

  IF HeaderOffset THEN                  'if there's a comment, extract it
    CommentLen = CVI(MID$(Temp$, HeaderOffset + 20, 2))
    GetComment$ = MID$(Temp$, HeaderOffset + 22, CommentLen)
  END IF

  Temp$ = ""                            'free up the memory

END FUNCTION

FUNCTION GetDir$ STATIC
 
  Regs.AX = &H4700                      'DOS get directory service
  Regs.DX = 0                           'the drive goes in DL, 0 = default
  Regs.SI = VARPTR(ZBuffer$)            'show DOS where ZBuffer$ is
  CALL Interrupt(DOS, Regs, Regs)       'call DOS
 'CALL Interrupt(DOS, Regs)             'use this with P.D.Q.
 
  IF Regs.Flags AND 1 THEN              'must be an invalid drive
    GetDir$ = ""
  ELSE
    Temp = INSTR(ZBuffer$, Zero$)       'find the zero byte, and return only
    GetDir$ = "\" + LEFT$(ZBuffer$, Temp - 1) ' what precedes it
  END IF

END FUNCTION

FUNCTION GetDrive% STATIC
 
  Regs.AX = &H1900                      'DOS Get Current Drive service
  CALL Interrupt(DOS, Regs, Regs)       'call DOS
 'CALL Interrupt(DOS, Regs)             'use this with P.D.Q.
  GetDrive% = (Regs.AX AND 255) + 65    'drive returned in AL as 0=A, 1=B...

END FUNCTION

FUNCTION IntVal% (Work$) STATIC

 'IntVal is an integer-only VAL substitute that reduces .EXE size up to 10K

  Length = LEN(RTRIM$(Work$))
  Value = 0

  FOR X = Length TO 1 STEP -1
    Temp = MidChar%(Work$, X)
    IF Temp > 47 AND Temp < 58 THEN
      IF X = Length THEN
        Value = Temp - 48
      ELSE
        Value = Value + (Temp - 48) * 10
      END IF
    END IF
  NEXT
 
  IntVal% = Value

END FUNCTION

FUNCTION MakeDir% (DirName$) STATIC

  ZBuffer$ = DirName$ + Zero$           'make an ASCIIZ string
  Regs.AX = &H3900                      'DOS create directory service
  Regs.DX = VARPTR(ZBuffer$)            'show DOS where ZBuffer$ is
  CALL Interrupt(DOS, Regs, Regs)       'call DOS
 'CALL Interrupt(DOS, Regs)             'use this with P.D.Q.

  IF Regs.Flags AND 1 THEN              'must be an invalid drive or bad name
    MakeDir% = -1                       'return -1 as an error
  END IF

END FUNCTION

FUNCTION MidChar% (Work$, Position)

  IF Position <= LEN(Work$) THEN
    MidChar% = ASC(MID$(Work$, Position, 1))
  ELSE
    MidChar% = -1
  END IF

END FUNCTION

SUB MidCharS (Work$, Position, NewChar) STATIC

  MID$(Work$, Position, 1) = CHR$(NewChar)

END SUB

FUNCTION Prompt% (Which) STATIC

  SHARED HiFG, HiBG, MonoMon, CsrSize, DestPath$

  DEF SEG = &HB800                      'assume a color display
  IF MonoMon THEN DEF SEG = &HB000      'nope, use the mono video segment

  REDIM SaveScrn(10 TO 13, 14 TO 66)    'this saves the underlying screen
  FOR Row = 10 TO 13                    'Here, Row and Col are zero-based
    FOR Col = 14 TO 66
      Temp = Row * 160 + Col * 2        'calculate the address just once
      SaveScrn(Row, Col) = PEEK(Temp) + 256 * PEEK(Temp + 1)
    NEXT
  NEXT

  COLOR HiFG, HiBG
  CALL DrawBox(11, 15, 14, 67, One)     'draw the surrounding box
  IF Which THEN                         'we were called from EarlyEnd
    LOCATE 12, 31                       'show this directory name
    PRINT "Are you sure you want";      'print the prompt message
    LOCATE 13, 29, 1, CsrSize - 1, CsrSize
    PRINT "to quit installing? (Y/N) ";
  ELSE                                  'prompt if okay to overwrite files
    LOCATE 12, 17                       'show this directory name
    PRINT "Installing to "; DestPath$   'print the prompt message
    LOCATE 13, 17, 1, CsrSize - 1, CsrSize
    PRINT "Prompt before overwriting existing files? (Y/N) ";
  END IF
  DO                                    'wait for Yes or No (only)
    Temp$ = UCASE$(INKEY$)
  LOOP UNTIL INSTR(" YN", Temp$) > 1
  Prompt% = 0                           'assume the answer is No
  IF Temp$ = "Y" THEN Prompt% = -1      'they answered Yes

  FOR Row = 10 TO 13                    'now restore the screen
    FOR Col = 14 TO 66                  'as above
      Temp = Row * 160 + Col * 2
      POKE Temp, SaveScrn(Row, Col) AND 255
      POKE Temp + 1, SaveScrn(Row, Col) \ 256
    NEXT
  NEXT

  ERASE SaveScrn
  LOCATE 2                              'put cursor at the top of the screen

END FUNCTION

SUB ReadNames (Spec$, Array$()) STATIC  'reads file names into an array

  ZBuffer$ = Spec$ + Zero$              'make an ASCIIZ string of the spec
  CurFile = 0                           'zero out the file counter

  Regs.DX = VARPTR(ZBuffer$)            'the file spec address
  Regs.CX = 39                          'file attribute = all files
  Regs.AX = &H4E00                      'find first matching name service

  DO
    CALL Interrupt(DOS, Regs, Regs)     'see if there's a match
   'CALL Interrupt(DOS, Regs)           'use this with P.D.Q.
    IF Regs.Flags AND 1 THEN EXIT DO    'no more files

    CurFile = CurFile + 1               'we found another file name
    Array$(CurFile) = SPACE$(12)        'create the string to hold it
    Temp$ = DTA.FileName                'assign the name
    Temp = INSTR(Temp$, Zero$)          'find the terminating zero byte
    LSET Array$(CurFile) = LEFT$(Temp$, Temp - 1) 'keep only what precedes it

    Regs.AX = &H4F00                    'find the next name
  LOOP

END SUB

SUB SelectFiles (FileNames$(), Choice, ExitCode) STATIC

  SHARED NumFiles, NormFG, NormBG, HiFG, HiBG, MainFG, MainBG

  IF ExitCode = -60 THEN EXIT SUB       'we got here via F2 pressed in Editor

  COLOR MainFG, MainBG                  'first display all of the choices
  LOCATE 25, 2                          'and update the status line
  PRINT "Up/Down/Space: Select files    Tab: Edit destination    F2: Begin    Esc: Quit";

  COLOR NormFG, NormBG
  FOR Temp = 1 TO NumFiles
    LOCATE 4 + Temp, 28, 0              'and turn off the cursor
    PRINT FileNames$(Temp);
  NEXT

  IF Choice = 0 THEN Choice = 1         'start at element 1 if first time
  IF Choice > UBOUND(FileNames$) THEN Choice = 1 'or if past the end

  DO
    LOCATE 4 + Choice, 28               'redraw current choice highlighted
    COLOR HiFG, HiBG
    PRINT FileNames$(Choice);
    DO
      KeyHit$ = INKEY$                  'see what they want to do
    LOOP UNTIL LEN(KeyHit$)             'wait for a keypress
   
    IF LEN(KeyHit$) = 1 THEN            'set ExitCode based on the type of
      ExitCode = ASC(KeyHit$)            'key (extended or not) they pressed
    ELSE
      ExitCode = -ASC(MID$(KeyHit$, 2))
    END IF
   
    IF ExitCode = 32 THEN               'spacebar
      IF MidChar%(FileNames$(Choice), 2) = 251 THEN 'if it's now checked
        Temp = 32                       'remove the check mark
      ELSE
        Temp = 251                       'else add a check mark
      END IF
      CALL MidCharS(FileNames$(Choice), 2, Temp)
      ExitCode = -80                    'select the next file automatically
    END IF

    SELECT CASE ExitCode
      CASE -80                          'Down Arrow
        GOSUB Deselect
        Choice = Choice + 1
        IF Choice > NumFiles THEN Choice = 1
      CASE -79                          'End key
        GOSUB Deselect
        Choice = NumFiles
      CASE -72                          'Up Arrow
        GOSUB Deselect
        Choice = Choice - 1
        IF Choice = 0 THEN Choice = NumFiles
      CASE -71                          'Home
        GOSUB Deselect
        Choice = 1
      CASE -60                          'F2
        EXIT SUB
      CASE 27                           'Escape
        EXIT SUB
      CASE 9                            'Tab
        LOCATE Choice + 4, 33
        GOSUB Deselect
        EXIT SUB
      CASE ELSE                         'this is needed for QB 4.0 only
    END SELECT
  LOOP

Deselect:                               're-paint the current choice, so it
  LOCATE Choice + 4, 28                 '  won't appear active
  COLOR NormFG, NormBG
  PRINT FileNames$(Choice);
  RETURN

END SUB

SUB SetDrive (Drive$) STATIC
   
  Regs.AX = &HE00                       'DOS Set Drive service in AH
  Regs.DX = ASC(UCASE$(Drive$)) - 65    'DL = 0 for A:, 1 for B:, and so on

  CALL Interrupt(DOS, Regs, Regs)       'see if there's a match
 'CALL Interrupt(DOS, Regs)             'use this with P.D.Q.

END SUB

FUNCTION SourceDir$ STATIC

  Temp$ = ExeName$                  'get the directory we're running from

  FOR X = LEN(Temp$) TO 1 STEP -1   'isolate the drive letter and path
    Temp = MidChar%(Temp$, X)       '  (strip off the name PC-SETUP.EXE)
    IF Temp = 58 OR Temp = 92 THEN  'look for a colon or a backslash
      SourceDir$ = LEFT$(Temp$, X)  'by searching for ":" or "\" this will
      EXIT FOR                      ' work even if the program is renamed
    END IF
  NEXT

END FUNCTION

SUB StuffBuf (Cmd$) STATIC
       
  '----- Set the segment for poking, define the buffer head and tail, and
  '      then poke each character into the keyboard buffer.
 
  Temp = LEN(Cmd$)

  DEF SEG = 0
  POKE &H41A, &H1E
  POKE &H41C, &H1E + Temp * 2
   
  FOR X = 1 TO Temp
    POKE &H41C + X * 2, ASC(MID$(Cmd$, X))
  NEXT

END SUB

