'===========================================================================
' Subject: TEXT FILE VIEWER                   Date: 01-30-97 (14:40)       
'  Author: Craig Wright                       Code: QB, QBasic, PDS        
'  Origin: e-mail anonymous                 Packet: TEXT.ABC
'===========================================================================
'Description
'  QBasic simple plain ASCii text file viewer for _BIG_ files
'  Theoretical max file size (using QBasic) is about 16000 x 64kb
'  or 1 gb, using typical real-world files reduces this to 512 mb

'  It was coded in response to a request from a beginner to show how
'  to view big files in QBasic and is _ONLY_ a shell <250 lines,
'  you can add whatever you want ; left/right scroll, mouse etc.

'  This is not claimed to be the ideal solution just another method.
'  I may have gone a bit overboard on the comments :)

'Pro's and Cons
'  Note that 16383 and 65535 have been rounded down.

'  It's slow when scrolling from each 64kb chunk although this can
'  be reduced in size to make loading quicker. Lots of wasteage due to
'  file white space/overlap, even worse if long lines >80 need to be
'  viewed.
'  Using Pointer&(1000) and Text(64000 \ 80) requires 68kb and
'  allows about 800 000 lines.
'  The memory overhead can be reduced - Pointer&(2000) and
'  Text(32000 \ 80) requires 40kb and allows 800 000 lines !

'  This is a best case, typical text files will not fill all of Text(),
'  assuming 50% wasteage in file white space and overlap still allows
'  400 000 lines.

'  For small files a file pointer array and SEEK to beginning of each
'  line may be more appropriate but you MUST be using a cached drive
'  and a hard disk, using this method with uncached drives or floppy,
'  ZiP drives etc. will be very slow.
'  Using Pointer1&(16000) and Pointer2&(16000) requires 128kb and
'  allows 32 000 lines.
'  16000 &longs = 64kb = max QBasic array size

'Notes
'  Instead of allocating an extra buffer (and associated code)
'  when scrolling down from one 64kb chunk of file to another, the
'  last bit of the previous 64kb chunk is used - overlap.
'  Code broken up to avoid word wrapping when >80 characters

'Limits
'  Only handles 80 character line lengths
'  May be a noticeable delay for large files when loading in file
'  chunks unless a disk cache such as SMARTDRV is used

'Bugs
'  Page Up/Dn needs sorting
'  Problem with displaying last file chunk if file>64kb, it'll
'  display but will also move up more than 1 line
'***************************************************************

'$DYNAMIC
DEFINT A-Z

DECLARE SUB BuildTable ()
DECLARE SUB CheckLimits ()
DECLARE SUB FindString (SearchFor$)
DECLARE SUB GetFileChunk ()
DECLARE SUB GetFileToView ()
DECLARE FUNCTION GetKey$ ()
DECLARE SUB ResetScreen ()
DECLARE SUB ViewFile ()


    'top and bottom rows of text mode view limits
CONST TopRow = 2
CONST BottomRow = 24
    'back and foreground colours valid values are 1-15
CONST BackColour = 1
CONST ForeColour = 7

    'transition buffer size/overlap (in lines) when loading
    'from one 64kb chunk of file to another
CONST TransitionLines = BottomRow - TopRow + 1

    'Allocate 64kb to hold all or part of file, if using in your
    'own code then set it to your preferred value and use REDIM
    'to de-allocate memory. Note that reducing its size will also
    'reduce delay between loading in each file chunk.
    '64kb = maximum size in Qbasic.
    'QuickBasic/PDS/VB for DOS users can increase this size
DIM SHARED Text(64000 \ 80) AS STRING * 80

    'file position where 64kb chunks read from, max file size
    'using 1000 is about 1000 x 64kb, increase 1000 to view
    'larger files
DIM SHARED Pointer(1000) AS LONG
   
    'points to current 64kb chunk=Text() of file being viewed
DIM SHARED CurrentFilePosition AS INTEGER
    'total number of 64kb chunks in file
DIM SHARED TotalChunks AS INTEGER, FileName$, FileHandle AS INTEGER
DIM SHARED MaxLines AS INTEGER, LinesInText AS LONG, CurrentLine


KEY OFF
VIEW PRINT

      'Kludge - increase typematic/scroll rate, only works if
      'MS DOS v6.xx MODE.COM available in the path and CMOS not
      'disabled it, different syntax required for other DOS's
      'Makes big difference to scroll rate if typematic rate is slow
      'Win v3.xx automatically increases typematic rate in DOS boxes
      'For QB/PDS/VB use a call interrupt
    SHELL "mode con rate=32 delay=1"

    CLS
       
        'get total lines in array buffer
    LinesInText = UBOUND(Text)

        'set to 1st 64kb chunk in file
    CurrentFilePosition = 1

    GetFileToView
   
    BuildTable
   
        'load in first 64kb chunk
    GetFileChunk
     
    ViewFile

    ResetScreen
    END


    '************* error handlers
BadPath:
ResetScreen
PRINT "Error ! unable to access your selected file -"
PRINT "  "; FileName$
PRINT
PRINT "An invalid path, drive or file name may have been used": END

BadErase:
ResetScreen
PRINT "Error ! unable to access disk": END

InitializeError:
ResetScreen
PRINT "Error ! unknown problem occurred when loading in file": END

ProgramError:
ResetScreen
PRINT "Error ! internal program error occurred": END

REM $STATIC
SUB BuildTable
  
    'builds the pointer table for byte position in file
    'where each 64kb chunk is to be loaded from

    ON ERROR GOTO InitializeError
    PRINT
     
      'check if valid text file by getting line length of first
      'line, should really check all lines but this may be too
      'slow for big files
    LINE INPUT #FileHandle, a$

    IF LEN(a$) > 80 THEN
        PRINT "Warning ! file contains lines that exceed 80 characters"
        PRINT "in length, it may not be a valid plain ASCii text file"
        PRINT
        PRINT "Press a key to continue or Q to quit"
        PRINT
        z$ = UCASE$(INPUT$(1))
        IF z$ = "Q" THEN END
    END IF
   
    PRINT : PRINT "Please wait - loading in file"
   
    SEEK #FileHandle, 1

        'first 64kb chunk of file = 1
    TotalChunks = 1
        'first chunk in file always starts at byte 1
    Pointer(TotalChunks) = 1

    DO WHILE NOT EOF(FileHandle)
        LINE INPUT #FileHandle, a$
        i = i + 1
       
        IF i > LinesInText - TransitionLines THEN
                'finished counting this 64kb chunk
            TotalChunks = TotalChunks + 1
            Pointer(TotalChunks) = SEEK(FileHandle)
               
                'check exceeds table array size
            IF TotalChunks = UBOUND(Pointer) THEN EXIT DO
           
                'show table being built, only needed for
                'large files and slow systems
            PRINT "±";
            i = 0
        END IF
  
    LOOP
   
    CLS

END SUB

SUB CheckLimits
       
        'moving up through file
        IF CurrentLine < 1 THEN
            CurrentLine = 1
            
                'if file is >64kb then load in previous 64 kb chunk
            IF TotalChunks > 1 THEN
                IF CurrentFilePosition > 1 THEN
                      'not at file beginning so load in previous
                      '64 kb chunk of file
                    CurrentFilePosition = CurrentFilePosition - 1
                    GetFileChunk
                    CurrentLine = MaxLines - TransitionLines + 1
                END IF
            END IF
        END IF
     
        'moving down through file
        IF CurrentLine > MaxLines - TransitionLines + 1 THEN
            CurrentLine = MaxLines - TransitionLines + 1
      
            IF TotalChunks > 1 THEN
                IF CurrentFilePosition < TotalChunks THEN
                        'not at file end
                    CurrentFilePosition = CurrentFilePosition + 1
                    GetFileChunk
                    CurrentLine = 1
                END IF
            END IF
        END IF
     
        IF CurrentLine < 1 THEN CurrentLine = 1
        IF CurrentLine > MaxLines THEN CurrentLine = MaxLines

END SUB

SUB FindString (SearchFor$)

        'gotta have something to search for
    IF LEN(SearchFor$) < 1 THEN EXIT SUB
   
        'get original line and file position
    LastLine = CurrentLine
    LastPosition = CurrentFilePosition
       
        'make it case insensitive
    SearchFor$ = LCASE$(SearchFor$)
       
    LOCATE 1, 2, 0: PRINT "Searching "
   
    DO
            'show search in progress
        LOCATE 1, 13: PRINT USING "####"; CurrentLine
       
        CurrentLine = CurrentLine + 1
           
            'check if need to load in next bit of file
        IF CurrentFilePosition < TotalChunks THEN CheckLimits
       
        IF INSTR(1, LCASE$(Text(CurrentLine)), SearchFor$) > 0 THEN EXIT SUB
           
            'quit if last file chunk and last line in file
        IF CurrentLine >= MaxLines THEN EXIT DO
    LOOP

   
        'nothing found so reset to original position in file
    CurrentLine = LastLine
    CurrentFilePosition = LastPosition
    GetFileChunk
    CheckLimits
    LOCATE 1, 2: PRINT "No matches found !"
    BEEP
    z$ = GetKey

END SUB

SUB GetFileChunk
    
    'loads in a 64kb chunk of the file at a time
   
        'make sure buffer is blank in case at end of file
    FOR i = 1 TO LinesInText
        Text(i) = ""
    NEXT

        'go to file position where 64kb chunk to be loaded from
    SEEK #FileHandle, Pointer(CurrentFilePosition)

    MaxLines = 0

        'FOR loop not used as there may not be enough lines
        'to fill the buffer
    DO WHILE NOT EOF(FileHandle)
        LINE INPUT #FileHandle, a$
        
        MaxLines = MaxLines + 1

            'Strip out form feeds - needs more checking
        DO
            i = INSTR(a$, CHR$(12))
            IF i > 0 THEN MID$(a$, i, 1) = " "
        LOOP UNTIL i = 0
       
            'strip out TABs
        DO
            i = INSTR(a$, CHR$(9))
            IF i > 0 THEN MID$(a$, i, 1) = " "
        LOOP UNTIL i = 0

        Text(MaxLines) = a$
           
            'finished loading in this chunk
        IF MaxLines = LinesInText THEN EXIT DO
    LOOP

END SUB

SUB GetFileToView
   
        'get file to view and check it exists
   
    PRINT : INPUT "Enter file to view : ", FileName$
   
    IF LEN(FileName$) < 1 THEN
        PRINT : PRINT "Error ! no file selected": END
    END IF

        'check file exists - trap invalid path, drive and file names
    ON ERROR GOTO BadPath
    FileHandle = FREEFILE
    OPEN FileName$ FOR BINARY AS #FileHandle
          
        'Open in binary mode will create a new file if none
        'exists, this is 0 bytes long - existing files are
        'usually >0 bytes long
    IF LOF(FileHandle) = 0 THEN
        CLOSE FileHandle
        PRINT "Error ! your selected file does not exist !"
        PRINT
            'clean up by erasing the new, zero length file
        ON ERROR GOTO BadErase
        KILL FileName$
        END
    END IF

    CLOSE FileHandle

        'file actually exists so open it in text mode
    FileHandle = FREEFILE
    OPEN FileName$ FOR INPUT AS #FileHandle

END SUB

FUNCTION GetKey$
   
        'returns a key press
    DO
        z$ = INKEY$
        LOCATE 1, 72: PRINT TIME$
    LOOP UNTIL z$ > ""
   
    GetKey$ = UCASE$(z$)

END FUNCTION

SUB ResetScreen
    'close all open files, reset colours, clear screen before
    'exit and switch on cursor
    CLOSE : COLOR 7, 0: CLS : LOCATE , , 0: PRINT
END SUB

SUB ViewFile

        'initial start position in array buffer
    CurrentLine = 1
        'total lines displayed on the screen
    LinesDisplayed = BottomRow - TopRow

        'draw border & useful info
    COLOR 0, 7
    LOCATE 1, 1: PRINT SPACE$(80);
    LOCATE 25, 1: PRINT SPACE$(80);
    LOCATE 1, 35: PRINT "Press H for Help - ESCape quits";
    LOCATE 25, 2: PRINT "File : "; LEFT$(FileName$, 50);
    LOCATE 25, 60: PRINT "Size : "; LOF(FileHandle) \ 1000; "kb";

        'main view loop
    ON ERROR GOTO ProgramError

    DO
            'show info about buffer
        COLOR 0, 7
        LOCATE 1, 2, 0: PRINT "Chunk No/Line :";
        PRINT CurrentFilePosition; "/"; CurrentLine; "   "
        COLOR ForeColour, BackColour
        
          'display the file - as Text() is fixed length string
          'it will erase previously displayed lines - no erase
          'line routine or padding out to end of line required
        FOR i = 0 TO LinesDisplayed
            LOCATE TopRow + i, 1: PRINT Text(CurrentLine + i);
        NEXT

        SELECT CASE GetKey
            CASE CHR$(0) + CHR$(72)         'Up
                CurrentLine = CurrentLine - 1
           
            CASE CHR$(0) + CHR$(80)         'Down
                CurrentLine = CurrentLine + 1
           
            CASE CHR$(0) + CHR$(73)         'Page Up
                CurrentLine = CurrentLine - LinesDisplayed
           
            CASE CHR$(0) + CHR$(81)         'Page Down
                CurrentLine = CurrentLine + LinesDisplayed
           
            CASE CHR$(0) + CHR$(71)         'Home
                CurrentLine = 0             'force update
                IF TotalChunks > 1 THEN CurrentFilePosition = 2
                CheckLimits
                CurrentLine = 1
           
            CASE CHR$(0) + CHR$(79)         'Endkey
                CurrentLine = MaxLines - TransitionLines + 2
                CurrentFilePosition = TotalChunks - 1
                CheckLimits
                CurrentLine = MaxLines - TransitionLines + 1
           
            CASE CHR$(13), CHR$(27)
                    'Return or ESCape key exits main loop
                EXIT DO
           
            CASE "F"
                COLOR 0, 7: LOCATE 1, 1: PRINT SPACE$(80);
                COLOR 7, 0
                LOCATE 1, 2, 1:
                INPUT "Enter text to search for : ", SearchFor$
                COLOR 0, 7: LOCATE 1, 1: PRINT SPACE$(80);
                FindString (SearchFor$)
                COLOR 0, 7: LOCATE 1, 1: PRINT SPACE$(80);
           
            CASE "R"
                    'repeat last find
                FindString (SearchFor$)
                COLOR 0, 7: LOCATE 1, 1: PRINT SPACE$(80);
           
            CASE "H"
                COLOR 7, 0
                FOR i = 7 TO 18     'clear partial screen
                    LOCATE i, 9: PRINT SPACE$(60)
                NEXT
                LOCATE 8, 11: PRINT "Help"
                LOCATE 9, 13: PRINT "Up/Down    - move"
                LOCATE 10, 13: PRINT "PageUp/Dn  - Page Up and Page Down"
                LOCATE 11, 13: PRINT "Home       - go to file beginning"
                LOCATE 12, 13: PRINT "End        - go to file end"
                LOCATE 13, 13: PRINT "CR/ESCape  - quit"
                LOCATE 14, 13: PRINT "F          - find text string, case insensitive"
                LOCATE 15, 13: PRINT "R          - repeat last find"
                LOCATE 16, 13: PRINT "H          - help !"
                z$ = GetKey

        END SELECT
      
        'bit inefficient this - calls it twice
       CheckLimits
    LOOP

END SUB
