'===========================================================================
' Subject: TEXT EDITOR                        Date: 03-11-96 (01:10)       
'  Author: Douglas H. Lusher                  Code: QB, PDS                
'  Origin: FidoNet QUIK_BAS Echo            Packet: TEXT.ABC
'===========================================================================
'A few comments are called for, in no particular order:
'1) this text editor works by loading a file into a string array
'and allowing the user to manipulate it there. The limit on file
'size, then, is the amount of string memory available. In compiled
'form, this will be about 57K. Run in the environment, this will
'be about 40K. I have worked quite hard to prevent "Out of memory"
'errors from crashing the program, but it may not be
'quite perfect yet. You will note a variable named "MemPad"
'at various places in the code. The program always leaves a
'small amount of memory free for use by QB's built-in routines.
'I am working on an editor that will handle much larger file
'sizes, but that code is still very buggy and I will not
'inflict my buggy code on the world.

'2) when compiled as-is for a stand-alone text editor, it will
'produce about a 56K file. I am working on eliminating all use
'of the LINE INPUT command, and when successful, the compiled
'size should go down about 10K because the floating point emula-
'tion routines will no longer be linked in.

'3) please understand that I wrote this editor for my own use.
'I needed something for doing mail, small batch files, etc. I
'don't have very much money, so I didn't want to buy something.
'For a while I was using TED, the editor from PC magazine, but
'I do so much of my work in QB and am so accustomed to the way
'the QB editor works that I found using TED to be quite annoying.
'My fingers just naturally wanted to go where they were accustommed
'to for QB. So finally, fancying myself a moderately competent
'programmer, I decided to write an editor for myself that worked
'like QB's. This code is the result. I will listen to suggestions
'for improvement, but please understand that I will make only
'such changes as suit *me*. If you want it to work differently,
'you'll have to rework the code to suit yourself. (Bug reports,
'of course, are always welcome.)

'4) There are, unfortunately, a dearth of comments, remarks, and
'explanations in the code. I'm sorry. I have worked for quite a
'while on this code and it is very familiar to me, so comments
'were just getting in my way. And I have done everything possible
'to give names to variables and procedures that make clear their
'purpose in the code. I fancy the code is quite readable without
'many comments, but then, as I said, I'm very familiar with it,
'so I'm probably not a good judge.

'5) The whole trick to doing a text editor is that there are two
'things that you must keep track of: the location of the cursor
'on the screen, and the location of where you are in the file.
'Variables having to do with locations on the screen are given
'names that contain "Row" or "Col" depending on whether they
'refer to a row or a column on the screen. Variables with names
'containing "Ln" or "Line" have to do with the lines in the
'file (the same as the elements of the string array which contain
'the text). Variables whose names contain "Char" refer to
'character positions in the lines of the text. CursRow%, CursCol%
'are the current location of the cursor on the screen. ActLn%
'is the line being edited. ActChar% is the character within that
'line where editing is taking place. Note that the active line
'is copied to a scalar string variable (named ActLn$) for speed
'and ease of reference. Variable names that contain
'the letters "Ptr" are just generic pointers to lines or
'characters. Hopefully, this information will help you to follow
'the flow of the code.
'   These variables control how much of the screen the editor
'covers:
'  TopRow% = 1: BotRow% = 25: LeftCol% = 1: RiteCol% = 80
'these values, of course, make it take the whole screen. Simply
'by changing these values you can edit text on only part of the
'screen if you wish. Note, however, that code assumes that
'dialog boxes can be centered in the editing area of the screen.
'If the screen window for editing is made too small, the dialog
'boxes will be chopped off. So if you want a very small editing
'area, you will have to rework that part of the code. As written,
'the screen window must be at least 72 columns wide and the
'total number of screen rows should not be less than 3.
'Oh, and when you shell out to DOS, it automatically takes the
'whole screen, so changes would have to be made there as well.

'6) You can load a file from the command line or from within
'the editor. Enter text from the keyboard. The arrow keys
'move the cursor from line to line or character to
'character. Home takes the cursor to the begining of the
'current line, the End key takes it to the end.
'PageUp and PageDn move the cursor the number of lines in the
'editing window. Control+Home takes you to the beginning of the
'file, Control+End takes you to the end. To mark a block, hold
'down a shift key and use the above listed keys to move the cursor
'as desired. When a block is marked, you can delete it using the
'delete key, or Shift+delete deletes it and moves it to the
'clipboard. Text in the clipboard can be inserted anywhere using
'Shift+Insert. If a block is marked, it can be printed by
'pressing Alt+P. A marked block can be saved to a file by
'pressing Alt+S. When no block is marked, pressing Alt+S
'saves the entire file to disk. Pressing Alt+L allows you to
'load a file into the editor. Alt+F allows you to search for
'text in the file. Alt+R displays a ruler on the screen that you
'can move up and down using the arrow keys. Pressing F10 will
'shell you out to DOS. F1 is the help key, for what it's worth.
'Use Alt+X or Alt+Q to quit. Alt+X assumes you will want to
'save the file, while Alt+Q gives you the option of exiting
'without saving the changes you made. I forgot to mention
'the Tab key and the Backspace key, they both work. The insert
'key toggles you between insert mode and overstrike mode. When
'there is a dialog box on screen, you can abort and return to
'the editor by pressing Escape.

'code for a text editor, written completely in QuickBASIC 4.5
'by Douglas H. Lusher, 03-11-1996
DEFINT A-Z
'$INCLUDE: 'qb.bi'
CONST False = 0, True = NOT False
CONST Black = 0, Blue = 1, Brown = 6, Grey = 7
CONST AltF = -33, AltL = -38, AltP = -25, AltQ = -16
CONST AltR = -19, AltS = -31, AltX = -45
CONST ESC = 27, ENTER = 13, BkSpc = 8, TabKey = 9
CONST F1Key = -59, F10Key = -68
CONST InsertKey = -82, DeleteKey = -83
CONST HomeKey = -71, CtrlHome = -119
CONST EndKey = -79, CtrlEnd = -117
CONST PgUpKey = -73, PgDnKey = -81
CONST UpArrow = -72, DnArrow = -80, LArrow = -75, RArrow = -77

DIM SHARED TopRow%, BotRow%, LeftCol%, RiteCol%
DIM SHARED ScrnRows%, MidRow%, ScrnWidth%
DIM SHARED FirstLn%, LastLn%, MaxLines%, FirstChar%, MaxChars%
DIM SHARED CursRow%, CursCol%, ActLn%, ActChar%
DIM SHARED FG%, BG%, BoxFG%, BoxBG%
DIM SHARED CursSize%, TabLen%, MemPad%, Null$

TopRow% = 1: BotRow% = 25: LeftCol% = 1: RiteCol% = 80
ScrnRows% = (BotRow% - TopRow%) + 1
MidRow% = TopRow% + (ScrnRows% \ 2)
ScrnWidth% = (RiteCol% - LeftCol%) + 1

FirstLn% = 1: LastLn% = 0: MaxLines% = 800
FirstChar% = 1: MaxChars% = 255

CursRow% = TopRow%: CursCol% = LeftCol%
ActLn% = FirstLn%: ActChar% = FirstChar%

FG% = Grey: BG% = Black: BoxFG% = Grey: BoxBG% = Brown
IF ColorMonitor% THEN
  CursSize% = 12: BG% = Blue
ELSE
  CursSize% = 7
END IF
TabLen% = 8: Null$ = "": MemPad% = 1024

DIM Text$(FirstLn% TO MaxLines% + 1)

 Cmd$ = RTRIM$(COMMAND$)
 Ptr% = INSTR(Cmd$, "/L:")
 IF Ptr% THEN
   Ptr2% = INSTR(Ptr% + 3, Cmd$, " ")
   IF Ptr2% THEN
	L$ = MID$(Cmd$, Ptr% + 3, Ptr2% - (Ptr% + 3))
	IF LEN(L$) THEN MaxLines% = VAL(L$)
	Cmd$ = MID$(Cmd$, 1, Ptr% - 1) + MID$(Cmd$, Ptr2% + 1)
   ELSE
	L$ = MID$(Cmd$, Ptr% + 3)
	IF LEN(L$) THEN MaxLines% = VAL(L$)
	Cmd$ = RTRIM$(MID$(Cmd$, 1, Ptr% - 1))
   END IF
 END IF
 File$ = Cmd$
 IF INSTR(File$, "*") OR INSTR(File$, "?") THEN
   BEEP: PRINT "No wildcards allowed in file specification": END
 END IF

IF LEN(File$) THEN
  IF FileExists(File$) THEN
    File = FREEFILE
    OPEN File$ FOR INPUT AS File
	 IF LOF(File) THEN
	   LnPtr% = 0
	   DO UNTIL EOF(File)
		LnPtr% = LnPtr% + 1
		IF LnPtr% > MaxLines% THEN
		  BEEP: PRINT "File has too many lines"
		  CLOSE File: END
		END IF
		LINE INPUT #File, Text$(LnPtr%)
		CALL Filter(Text$(LnPtr%))
		IF FRE(Null$) < MemPad% THEN
		  BEEP: PRINT "Not enough memory"
		  CLOSE File: END
		END IF
	   LOOP
	   LastLn% = LnPtr%
	 END IF
    CLOSE File
  ELSE
    IF NOT LegalFileName(File$) THEN
	 BEEP: PRINT "Bad file name": END
    END IF
  END IF
END IF

SCREEN 0, 1, 0: WIDTH 80
COLOR FG%, BG%, BG%: CLS

FileChanged% = False
DO
CALL Edit(Text$(), ExitKey%, FileChanged%)
SELECT CASE ExitKey%
  CASE AltS
    IF FileChanged% THEN
	 IF LEN(File$) THEN
	   GOSUB SaveFile
	 ELSE
	   GOSUB GetFileName
	   IF KeyCode% = ENTER THEN GOSUB SaveFile
	 END IF
    END IF
  CASE AltQ
    IF FileChanged% THEN
	 CALL MsgBox("Save file changes? (Y/N) ", "YyNn", KeyCode%)
	 SELECT CASE KeyCode%
	   CASE 89, 121                  '"Y", "y"
		IF LEN(File$) THEN
		  GOSUB SaveFile
		ELSE
		  GOSUB GetFileName
		  IF KeyCode% = ENTER THEN GOSUB SaveFile: EXIT DO
		END IF
	   CASE 78, 110                  '"N", "n"
		EXIT DO
	   CASE ESC
		'do nothing to return to edit mode
	 END SELECT
    ELSE
	 EXIT DO
    END IF
  CASE AltX
    IF FileChanged% THEN
	 GOSUB GetFileName
	 IF KeyCode% = ENTER THEN GOSUB SaveFile: EXIT DO
    ELSE
	 EXIT DO
    END IF
END SELECT
LOOP
CLS : END

GetFileName:
DO
  CALL InputBox("File name: ", File$, KeyCode%)
  IF KeyCode% = ENTER THEN
    IF LegalFileName(File$) THEN EXIT DO ELSE BEEP
  ELSE 'IF KeyCode% = ESC THEN
    EXIT DO
  END IF
LOOP
RETURN

SaveFile:
File = FREEFILE
OPEN File$ FOR OUTPUT AS File
  IF LastLn% > 0 THEN
    FOR LnPtr% = FirstLn% TO LastLn%
	 PRINT #File, Text$(LnPtr%)
    NEXT
  END IF
CLOSE File
FileChanged% = False
RETURN

FUNCTION ColorMonitor%
ColorMonitor% = True
DEF SEG = 0
IF PEEK(&H463) <> &HD4 THEN ColorMonitor% = False
DEF SEG
END FUNCTION

SUB DrawBox (TopRow%, NumRows%, LCol%, NumCols%)

Top$ = STRING$(NumCols%, "Í")
MID$(Top$, 1, 2) = " É": MID$(Top$, NumCols% - 1, 2) = "» "
Side$ = SPACE$(NumCols%)
MID$(Side$, 1, 2) = " º": MID$(Side$, NumCols% - 1, 2) = "º "
Bot$ = Top$
MID$(Bot$, 2, 1) = "È": MID$(Bot$, NumCols% - 1, 1) = "¼"

LOCATE TopRow%, LCol%, 0: PRINT Top$
FOR Row% = 3 TO NumRows%
  LOCATE , LCol%: PRINT Side$
NEXT
LOCATE , LCol%: PRINT Bot$;

END SUB

SUB Edit (Text$(), KeyCode%, FileChanged%)
'code for a text editor, written completely in QuickBASIC 4.5
'by Douglas H. Lusher, 03-11-1996

'when marking a block involving multiple lines:
BlkStartLn = 0        'is the starting line of the block
BlkLines = 0          'the number of lines in the block.
				  'If zero, then not in this block marking mode

'when marking a block of characters within a single line:
BlkStartChar = 0      'this is the starting character
BlkChars = 0          'the number of characters in the block.
				  'If zero, then not in this block marking mode

Refresh$ = SPACE$(ScrnWidth)       'scratch space
InsertMode = True
LOCATE , , 0, CursSize - 1, CursSize
GOSUB RefreshScreen
DO
  ActLn$ = Text$(ActLn): ActLnLen = LEN(ActLn$)
  LOCATE CursRow, CursCol, 1
  DO WHILE LEN(INKEY$): LOOP
  DO: Ky$ = INKEY$: LOOP UNTIL LEN(Ky$)
  DEF SEG = 0: ShiftByte = PEEK(&H417): DEF SEG
  ShiftPressed = ((ShiftByte AND 3) <> 0)
  KeyCode = ASC(Ky$): IF KeyCode = 0 THEN KeyCode = -ASC(MID$(Ky$, 2))
  SELECT CASE KeyCode
    CASE 32 TO 255, 1 TO 6, 14 TO 27: GOSUB DoLetterKey
    CASE BkSpc: GOSUB DoBackSpaceKey
    CASE TabKey: GOSUB DoTabKey
    CASE ENTER: GOSUB DoCarriageReturn
    CASE InsertKey: GOSUB DoInsertKey
    CASE DeleteKey: GOSUB DoDeleteKey
    CASE UpArrow: GOSUB DoUpArrow
    CASE DnArrow: GOSUB DoDnArrow
    CASE PgUpKey: GOSUB DoPgUpKey
    CASE PgDnKey: GOSUB DoPgDnKey
    CASE LArrow: GOSUB DoLArrow
    CASE RArrow: GOSUB DoRArrow
    CASE HomeKey: GOSUB DoHomeKey
    CASE EndKey: GOSUB DoEndKey
    CASE CtrlHome: GOSUB DoCtrlHome
    CASE CtrlEnd: GOSUB DoCtrlEnd
    CASE F1Key: GOSUB DoHelp
    CASE F10Key: CLS : SHELL: GOSUB RefreshScreen
    CASE AltF: GOSUB FindText
    CASE AltL: GOSUB LoadFile
    CASE AltP: GOSUB DoPrint
    CASE AltR: GOSUB DoRuler
    CASE AltS: IF BlkLines THEN GOSUB SaveLines ELSE EXIT DO
    CASE AltQ, AltX: EXIT DO
    CASE ELSE: BEEP
  END SELECT
LOOP

EXIT SUB

DoCarriageReturn:
IF BlkLines OR BlkChars THEN BEEP: RETURN
IF LastLn = MaxLines THEN BEEP: RETURN
IF ActLn < LastLn THEN
  FOR LnPtr = LastLn TO ActLn + 1 STEP -1
    SWAP Text$(LnPtr), Text$(LnPtr + 1)
  NEXT
  Text$(ActLn) = LEFT$(ActLn$, ActChar - 1)
  Text$(ActLn + 1) = MID$(ActLn$, ActChar)
END IF
ActLn = ActLn + 1
ActChar = FirstChar
IF CursRow <> BotRow THEN CursRow = CursRow + 1
CursCol = LeftCol
LastLn = LastLn + 1
GOSUB RefreshScreen
FileChanged = True
RETURN

DoTabKey:
IF BlkLines OR BlkChars THEN BEEP: RETURN
IF ActLn > MaxLines THEN BEEP: RETURN
TabChars = TabLen - (ActChar MOD TabLen)
IF ActLnLen + TabChars > MaxChars THEN BEEP: RETURN
IF ActChar - 1 + TabChars > MaxChars THEN BEEP: RETURN
IF ActChar <= ActLnLen THEN
  Text$(ActLn) = LEFT$(ActLn$, ActChar - 1) + SPACE$(TabChars) + MID$(ActLn$, ActChar)
ELSE
  Text$(ActLn) = ActLn$ + SPACE$((ActChar - ActLnLen - 1) + TabChars)
END IF
ActChar = ActChar + TabChars
IF CursCol + TabChars <= RiteCol THEN
  CursCol = CursCol + TabChars
  GOSUB RefreshLine
ELSE
  GOSUB RefreshScreen
END IF
IF ActLn > LastLn THEN LastLn = ActLn
FileChanged = True
RETURN

DoDeleteKey:
IF BlkLines THEN
  IF BlkLines > 0 THEN
    ActLn = BlkStartLn
    CursRow = Greater(CursRow - BlkLines, TopRow)
  END IF
  ClipLines = ABS(BlkLines)
  BytesNeeded& = 0
  FOR LnPtr = ActLn TO ActLn + ClipLines - 1
    BytesNeeded& = BytesNeeded& + LEN(Text$(LnPtr)) + 6
  NEXT
  IF BytesNeeded& + MemPad > FRE(Null$) THEN ClipLines = 0: BEEP: RETURN
  REDIM ClipBoard$(ClipLines - 1)
  FOR LnPtr = 0 TO ClipLines - 1
    SWAP ClipBoard$(LnPtr), Text$(ActLn + LnPtr)
  NEXT
  FOR LnPtr = ActLn TO LastLn - ClipLines
    SWAP Text$(LnPtr), Text$(LnPtr + ClipLines)
  NEXT
  BlkLines = 0: ClipChars = 0
  LastLn = LastLn - ClipLines
  IF NOT ShiftPressed THEN ClipLines = 0: ERASE ClipBoard$
  GOSUB RefreshScreen
ELSEIF BlkChars THEN
  IF BlkChars > 0 THEN
    ActChar = BlkStartChar
    CursCol = Greater(CursCol - BlkChars, LeftCol)
  END IF
  ClipChars = ABS(BlkChars)
  ClipBoard$ = MID$(ActLn$, ActChar, ClipChars)
  Text$(ActLn) = LEFT$(ActLn$, ActChar - 1) + MID$(ActLn$, ActChar + ClipChars)
  BlkChars = 0: ClipLines = 0
  IF NOT ShiftPressed THEN ClipChars = 0: ClipBoard$ = Null$
  GOSUB RefreshScreen
ELSE 'no blocks marked
  IF ShiftPressed THEN BEEP: RETURN
  IF ActChar <= ActLnLen THEN
    Text$(ActLn) = LEFT$(ActLn$, ActChar - 1) + MID$(ActLn$, ActChar + 1)
    GOSUB RefreshLine
  ELSE 'IF ActChar > ActLnLen THEN
    IF ActLn >= LastLn THEN BEEP: RETURN
    IF ActChar > MaxChars THEN BEEP: RETURN
    IF (ActChar - 1) + LEN(Text$(ActLn + 1)) > MaxChars THEN BEEP: RETURN
    Text$(ActLn) = ActLn$ + SPACE$(ActChar - ActLnLen - 1) + Text$(ActLn + 1)
    Text$(ActLn + 1) = Null$
    FOR LnPtr = ActLn + 1 TO LastLn
	 SWAP Text$(LnPtr), Text$(LnPtr + 1)
    NEXT
    LastLn = LastLn - 1
    GOSUB RefreshScreen
  END IF
END IF
FileChanged = True
RETURN

DoInsertKey:
IF ShiftPressed THEN
  IF ClipLines THEN
    IF LastLn + ClipLines > MaxLines THEN BEEP: RETURN
    BytesNeeded& = 0
    FOR LnPtr = 0 TO ClipLines - 1
	 BytesNeeded& = BytesNeeded& + LEN(ClipBoard$(LnPtr))
    NEXT
    IF BytesNeeded& + MemPad > FRE(Null$) THEN BEEP: RETURN
    IF ActLn <= LastLn THEN
	 FOR LnPtr = LastLn TO ActLn STEP -1
	   SWAP Text$(LnPtr), Text$(LnPtr + ClipLines)
	 NEXT
    END IF
    FOR LnPtr = 0 TO ClipLines - 1
	 Text$(ActLn + LnPtr) = ClipBoard$(LnPtr)
    NEXT
    LastLn = LastLn + ClipLines
    GOSUB RefreshScreen
    FileChanged = True
  ELSEIF ClipChars THEN
    IF ActLn > MaxLines THEN BEEP: RETURN
    IF ActLnLen + ClipChars > MaxChars THEN BEEP: RETURN
    IF ActChar + ClipChars - 1 > MaxChars THEN BEEP: RETURN
    IF ActChar <= ActLnLen THEN
	 Text$(ActLn) = LEFT$(ActLn$, ActChar - 1) + ClipBoard$ + MID$(ActLn$, ActChar)
    ELSE
	 Text$(ActLn) = ActLn$ + SPACE$(ActChar - ActLnLen - 1) + ClipBoard$
    END IF
    IF ActLn > LastLn THEN LastLn = ActLn
    GOSUB RefreshLine
    FileChanged = True
  END IF
ELSE 'IF NOT ShiftPressed THEN
  InsertMode = NOT InsertMode
  IF InsertMode THEN
    LOCATE , , , CursSize - 1, CursSize
  ELSE
    LOCATE , , , CursSize \ 2, CursSize
  END IF
END IF
RETURN

DoBackSpaceKey:
IF BlkLines OR BlkChars THEN BEEP: RETURN
IF ActChar = FirstChar THEN
  IF ActLn = FirstLn THEN BEEP: RETURN
  IF ActLnLen + LEN(Text$(ActLn - 1)) > MaxChars THEN BEEP: RETURN
  ActChar = LEN(Text$(ActLn - 1)) + 1
  Text$(ActLn - 1) = Text$(ActLn - 1) + ActLn$
  Text$(ActLn) = Null$
  FOR LnPtr = ActLn TO LastLn
    SWAP Text$(LnPtr), Text$(LnPtr + 1)
  NEXT
  IF LastLn >= ActLn THEN LastLn = LastLn - 1
  ActLn = ActLn - 1
  IF CursRow <> TopRow THEN CursRow = CursRow - 1
  CursCol = Lesser(ActChar, RiteCol)
  GOSUB RefreshScreen
ELSE 'IF ActChar > FirstChar THEN
  IF ActChar <= ActLnLen + 1 THEN
    Text$(ActLn) = LEFT$(ActLn$, ActChar - 2) + MID$(ActLn$, ActChar)
  END IF
  ActChar = ActChar - 1
  IF CursCol <> LeftCol THEN
    CursCol = CursCol - 1
    GOSUB RefreshLine
  ELSE
    GOSUB RefreshScreen
  END IF
END IF
FileChanged = True
RETURN

DoLetterKey:
IF BlkLines OR BlkChars THEN BEEP: RETURN
IF ActLn > MaxLines THEN BEEP: RETURN
IF ActChar > MaxChars THEN BEEP: RETURN
IF ActChar <= ActLnLen THEN
  IF InsertMode THEN
    IF ActLnLen = MaxChars THEN BEEP: RETURN
    Text$(ActLn) = LEFT$(ActLn$, ActChar - 1) + " " + MID$(ActLn$, ActChar)
  END IF
  MID$(Text$(ActLn), ActChar) = Ky$
ELSE 'IF ActChar > ActLnLen THEN
  Text$(ActLn) = ActLn$ + SPACE$(ActChar - ActLnLen - 1) + Ky$
END IF
ActChar = ActChar + 1
IF CursCol <> RiteCol THEN
  CursCol = CursCol + 1
  GOSUB RefreshLine
ELSE
  GOSUB RefreshScreen
END IF
IF ActLn > LastLn THEN LastLn = ActLn
FileChanged = True
RETURN

DoUpArrow:
BlkChars = 0
IF ShiftPressed THEN
  IF BlkLines = 0 THEN BlkStartLn = ActLn
  IF ActLn > FirstLn THEN BlkLines = BlkLines - 1
ELSE
  BlkLines = 0
END IF
IF ActLn <> FirstLn THEN
  ActLn = ActLn - 1
  IF CursRow <> TopRow THEN CursRow = CursRow - 1
END IF
GOSUB RefreshScreen
RETURN

DoDnArrow:
BlkChars = 0
IF ShiftPressed THEN
  IF BlkLines = 0 THEN BlkStartLn = ActLn
  IF ActLn <= LastLn THEN BlkLines = BlkLines + 1
ELSE
  BlkLines = 0
END IF
IF ActLn <= LastLn THEN
  ActLn = ActLn + 1
  IF CursRow <> BotRow THEN CursRow = CursRow + 1
END IF
GOSUB RefreshScreen
RETURN

DoPgUpKey:
BlkChars = 0
IF ShiftPressed THEN
  IF BlkLines = 0 THEN BlkStartLn = ActLn
  BlkLines = Greater(BlkLines - ScrnRows, FirstLn - BlkStartLn)
ELSE
  BlkLines = 0
END IF
ActLn = Greater(ActLn - ScrnRows, FirstLn)
IF ActLn < CursRow THEN CursRow = TopRow + ActLn - 1
GOSUB RefreshScreen
RETURN

DoPgDnKey:
BlkChars = 0
IF ShiftPressed THEN
  IF BlkLines = 0 THEN BlkStartLn = ActLn
  BlkLines = Lesser(BlkLines + ScrnRows, (LastLn + 1) - BlkStartLn)
ELSE
  BlkLines = 0
END IF
ActLn = Lesser(ActLn + ScrnRows, LastLn + 1)
IF (LastLn + 1) - ActLn < BotRow - CursRow THEN
  CursRow = Lesser(BotRow - ((LastLn + 1) - ActLn), TopRow + LastLn)
END IF
GOSUB RefreshScreen
RETURN

DoLArrow:
IF ShiftPressed THEN
  IF BlkLines THEN BEEP: RETURN
  IF BlkChars = 0 THEN BlkStartChar = ActChar
  BlkChars = Greater(BlkChars - 1, FirstChar - BlkStartChar)
ELSE
  BlkLines = 0: BlkChars = 0
END IF
IF ActChar <> FirstChar THEN
  ActChar = ActChar - 1
  IF CursCol <> LeftCol THEN CursCol = CursCol - 1
END IF
GOSUB RefreshScreen
RETURN

DoRArrow:
IF ShiftPressed THEN
  IF BlkLines THEN BEEP: RETURN
  IF BlkChars = 0 THEN BlkStartChar = ActChar
  IF ActChar <= MaxChars THEN BlkChars = BlkChars + 1
ELSE
  BlkLines = 0: BlkChars = 0
END IF
IF ActChar <= MaxChars THEN
  ActChar = ActChar + 1
  IF CursCol <> RiteCol THEN CursCol = CursCol + 1
END IF
GOSUB RefreshScreen
RETURN

DoHomeKey:
IF ShiftPressed THEN
  IF BlkLines THEN BEEP: RETURN
  IF BlkChars = 0 THEN BlkStartChar = ActChar
  BlkChars = FirstChar - BlkStartChar
ELSE
  BlkLines = 0: BlkChars = 0
END IF
ActChar = FirstChar
CursCol = LeftCol
GOSUB RefreshScreen
RETURN

DoEndKey:
IF ShiftPressed THEN
  IF BlkLines THEN BEEP: RETURN
  IF BlkChars = 0 THEN BlkStartChar = ActChar
  BlkChars = (ActLnLen + 1) - BlkStartChar
ELSE
  BlkLines = 0: BlkChars = 0
END IF
LeftChar = ActChar - CursCol + 1
ActChar = ActLnLen + 1
IF ActChar <= LeftChar THEN
  CursCol = Lesser(ActChar, RiteCol)
ELSE
  CursCol = Lesser(ActChar - LeftChar + 1, RiteCol)
END IF
GOSUB RefreshScreen
RETURN

DoCtrlHome:
BlkChars = 0
IF ShiftPressed THEN
  IF BlkLines = 0 THEN BlkStartLn = ActLn
  BlkLines = FirstLn - BlkStartLn
ELSE
  BlkLines = 0
END IF
ActLn = FirstLn
CursRow = TopRow
GOSUB RefreshScreen
RETURN

DoCtrlEnd:
BlkChars = 0
IF ShiftPressed THEN
  IF BlkLines = 0 THEN BlkStartLn = ActLn
  BlkLines = (LastLn + 1) - BlkStartLn
ELSE
  BlkLines = 0
END IF
ActLn = LastLn + 1
CursRow = Lesser(BotRow, TopRow + LastLn)
GOSUB RefreshScreen
RETURN

RefreshScreen:
LOCATE , , 0
LnPtr = ActLn - (CursRow - TopRow)
CharPtr = ActChar - (CursCol - LeftCol)
IF BlkLines THEN
  StartLn = BlkStartLn: StopLn = BlkStartLn + BlkLines
  IF StartLn > StopLn THEN SWAP StartLn, StopLn
  FOR Row = TopRow TO BotRow
    LSET Refresh$ = MID$(Text$(LnPtr), CharPtr, ScrnWidth)
    LOCATE Row, LeftCol
    IF LnPtr >= StartLn AND LnPtr < StopLn THEN
	 COLOR BG, FG: PRINT Refresh$; : COLOR FG, BG
    ELSE
	 PRINT Refresh$;
    END IF
    LnPtr = LnPtr + 1
  NEXT
ELSEIF BlkChars THEN
  StartChar = BlkStartChar: StopChar = BlkStartChar + BlkChars
  IF StartChar > StopChar THEN SWAP StartChar, StopChar
  LCol = Greater(StartChar - CharPtr, 0)
  RCol = StopChar - CharPtr
  FOR Row = TopRow TO BotRow
    LSET Refresh$ = MID$(Text$(LnPtr), CharPtr, ScrnWidth)
    LOCATE Row, LeftCol
    IF LnPtr = ActLn THEN
	 PRINT LEFT$(Refresh$, LCol);
	 COLOR BG, FG
	 PRINT MID$(Refresh$, LCol + 1, RCol - LCol);
	 COLOR FG, BG
	 PRINT MID$(Refresh$, RCol + 1);
    ELSE
	 PRINT Refresh$;
    END IF
    LnPtr = LnPtr + 1
  NEXT
ELSE
  FOR Row = TopRow TO BotRow
    LSET Refresh$ = MID$(Text$(LnPtr), CharPtr, ScrnWidth)
    LOCATE Row, LeftCol
    PRINT Refresh$;
    LnPtr = LnPtr + 1
  NEXT
END IF
RETURN

RefreshLine:
LSET Refresh$ = MID$(Text$(ActLn), ActChar - CursCol + LeftCol, ScrnWidth)
LOCATE CursRow, LeftCol, 0
IF BlkChars THEN
  StartChar = BlkStartChar: StopChar = BlkStartChar + BlkChars
  IF StartChar > StopChar THEN SWAP StartChar, StopChar
  LCol = Greater(StartChar - CharPtr, 0)
  RCol = StopChar - CharPtr
  PRINT LEFT$(Refresh$, LCol);
  COLOR BG, FG
  PRINT MID$(Refresh$, LCol + 1, RCol - LCol);
  COLOR FG, BG
  PRINT MID$(Refresh$, RCol + 1);
ELSE
  PRINT Refresh$;
END IF
RETURN

DoHelp:
Temp$ = STR$(ActChar): MID$(Temp$, 1, 1) = ":"
Msg$ = "Cursor" + STR$(ActLn) + Temp$ + " | Bytes free:" + STR$(FRE(Null$))
D = (ScrnWidth - LEN(Msg$)) \ 2 - 4
COLOR BoxFG, BoxBG
CALL DrawBox(MidRow% - 1, 3, LeftCol + D, LEN(Msg$) + 8)
LOCATE MidRow%, LeftCol + D + 4: PRINT Msg$;
DO UNTIL ASC(INPUT$(1)) = ESC: LOOP
Temp$ = Null$: Msg$ = Null$
COLOR FG, BG
GOSUB RefreshScreen
RETURN

LoadFile:
IF BlkLines OR BlkChars THEN BEEP: RETURN
IF ActLn > MaxLines THEN BEEP: RETURN
CALL InputBox("File name: ", LoadFile$, KeyCode)
GOSUB RefreshScreen
IF KeyCode <> ENTER THEN RETURN
LoadFile$ = LTRIM$(RTRIM$(LoadFile$))
IF LEN(LoadFile$) = 0 THEN RETURN
IF FileExists(LoadFile$) THEN
  FileLines = FileLineCount(LoadFile$)
  IF FileLines = 0 THEN RETURN
  IF LastLn + FileLines > MaxLines THEN
    CALL MsgBox("File has too many lines", Null$, KeyCode)
    GOSUB RefreshScreen: RETURN
  ELSEIF FileSize&(LoadFile$) + MemPad > FRE(Null$) THEN
    CALL MsgBox("Not enough memory", Null$, KeyCode)
    GOSUB RefreshScreen: RETURN
  END IF
ELSE
  BEEP: CALL MsgBox("File not found", Null$, KeyCode)
  GOSUB RefreshScreen: GOTO LoadFile
END IF

IF ActLn <= LastLn THEN
  FOR LnPtr = LastLn TO ActLn STEP -1
    SWAP Text$(LnPtr), Text$(LnPtr + FileLines)
  NEXT
END IF
File = FREEFILE
OPEN LoadFile$ FOR INPUT AS #File
  FOR LnPtr = ActLn TO ActLn + FileLines - 1
    LINE INPUT #File, FileLn$
    CALL Filter(FileLn$)
    Text$(LnPtr) = FileLn$
  NEXT
  FileLn$ = Null$
CLOSE File
LastLn = LastLn + FileLines
FileChanged = True
GOSUB RefreshScreen
RETURN

SaveLines:
CALL InputBox("File name: ", SaveFile$, KeyCode)
GOSUB RefreshScreen
IF KeyCode = ENTER THEN
  SaveFile$ = LTRIM$(RTRIM$(SaveFile$))
  IF LEN(SaveFile$) THEN
    IF FileExists(SaveFile$) THEN
	 CALL MsgBox("Specified file already exists: [A]ppend [O]verwrite [C]ancel? ", "AaOoCc", KeyCode)
	 GOSUB RefreshScreen
	 SELECT CASE KeyCode
	   CASE 65, 97: Mode$ = "A": GOSUB DoFileSave
	   CASE 79, 111: Mode$ = "O": GOSUB DoFileSave
	   CASE 67, 99, ESC    'do nothing to cancel the save
	 END SELECT
    ELSE
     IF LegalFileName(SaveFile$) THEN
	   Mode$ = "O": GOSUB DoFileSave
	 ELSE
	   CALL MsgBox("Bad file name", Null$, KeyCode)
	   GOSUB RefreshScreen: GOTO SaveLines
	 END IF
    END IF
  END IF
END IF
RETURN

DoFileSave:
File = FREEFILE
OPEN Mode$, File, SaveFile$
  StartLn = Lesser(ActLn, BlkStartLn)
  FOR LnPtr = StartLn TO StartLn + ABS(BlkLines) - 1
    PRINT #File, Text$(LnPtr)
  NEXT
CLOSE File
RETURN

FindText:
IF BlkLines THEN BEEP: RETURN
IF BlkChars THEN
  CharPtr = Lesser(BlkStartChar, BlkStartChar + BlkChars)
  Search$ = MID$(ActLn$, CharPtr, ABS(BlkChars))
  GOSUB Search
ELSE
  CALL InputBox("Find: ", Search$, KeyCode)
  GOSUB RefreshScreen
  IF KeyCode = ENTER THEN
    IF LEN(Search$) THEN GOSUB Search
  END IF
END IF
RETURN

Search:
Count = 0
LnPtr = ActLn: CharPtr = ActChar
DO
  CharPtr = INSTR(CharPtr, Text$(LnPtr), Search$)
  IF CharPtr THEN
    BlkStartChar = CharPtr
    BlkChars = LEN(Search$)
    ActChar = BlkStartChar + BlkChars
    CursCol = Lesser(LeftCol + ActChar - 1, RiteCol)
    TopLn = ActLn - (CursRow - TopRow)
    ActLn = LnPtr
    CursRow = MidRow
    IF ActLn >= TopLn AND ActLn < TopLn + ScrnRows THEN
	 CursRow = TopRow + (ActLn - TopLn)
    ELSEIF ActLn <= ScrnRows THEN
	 CursRow = TopRow + ActLn - 1
    ELSEIF (LastLn + 1) - ActLn < BotRow - CursRow THEN
	 CursRow = BotRow - ((LastLn + 1) - ActLn)
    END IF
    GOSUB RefreshScreen: EXIT DO
  ELSE
    Count = Count + 1
    IF Count > LastLn THEN
	 CALL MsgBox("Search string not found", Null$, KeyCode)
	 GOSUB RefreshScreen: EXIT DO
    END IF
    LnPtr = LnPtr + 1: IF LnPtr > LastLn THEN LnPtr = FirstLn
    CharPtr = 1
  END IF
LOOP
RETURN

DoPrint:
IF BlkLines THEN
  StartLn = Lesser(ActLn, BlkStartLn)
  StopLn = StartLn + ABS(BlkLines) - 1
ELSE
  StartLn = FirstLn
  StopLn = LastLn
END IF
FOR LnPtr = StartLn TO StopLn
  LPRINT Text$(LnPtr)
NEXT
BlkLines = 0
GOSUB RefreshScreen
RETURN

DoRuler:
IF BlkLines OR BlkChars THEN RETURN
Ruler$ = SPACE$(MaxChars)
FOR CharPtr = 1 TO MaxChars
  MID$(Ruler$, CharPtr, 1) = "Â"
  IF CharPtr MOD 5 = 0 THEN MID$(Ruler$, CharPtr) = "Å"
  IF CharPtr MOD 10 = 0 THEN
    MID$(Ruler$, CharPtr - 1) = CHR$(((CharPtr \ 10) MOD 10) + 48)
  END IF
NEXT
Ruler$ = MID$(Ruler$, ActChar - CursCol + LeftCol, ScrnWidth)

RulerRow = TopRow
DO
  GOSUB PrintRuler
  DO WHILE LEN(INKEY$): LOOP
  DO: Ky$ = INKEY$: LOOP UNTIL LEN(Ky$)
  KeyCode = ASC(Ky$): IF KeyCode = 0 THEN KeyCode = -ASC(MID$(Ky$, 2))
SELECT CASE KeyCode
    CASE UpArrow
	 IF RulerRow > TopRow THEN RulerRow = RulerRow - 1
    CASE DnArrow
	 IF RulerRow < BotRow THEN RulerRow = RulerRow + 1
    CASE PgUpKey
	 IF RulerRow <> TopRow THEN RulerRow = TopRow
    CASE PgDnKey
	 IF RulerRow <> BotRow THEN RulerRow = BotRow
    CASE ESC
	 EXIT DO
    CASE ELSE
	 BEEP
  END SELECT
LOOP
Ruler$ = Null$
GOSUB RefreshScreen
RETURN

PrintRuler:
GOSUB RefreshScreen
LOCATE RulerRow, LeftCol, 0
COLOR BG, FG: PRINT Ruler$; : COLOR FG, BG
RETURN

END SUB

FUNCTION FileExists% (FileName$)
DIM XRegister AS RegTypeX

'save the current DTA
XRegister.AX = &H2F00
CALL InterruptX(&H21, XRegister, XRegister)
OldDTASeg% = XRegister.ES
OldDTAOff% = XRegister.BX

'set up a new DTA
DTA$ = SPACE$(43)
XRegister.AX = &H1A00
XRegister.DS = VARSEG(DTA$)
XRegister.DX = SADD(DTA$)
CALL InterruptX(&H21, XRegister, XRegister)

'get first matching file
Temp$ = FileName$ + CHR$(0)
XRegister.AX = &H4E00
XRegister.CX = &H6
XRegister.DS = VARSEG(Temp$)
XRegister.DX = SADD(Temp$)
CALL InterruptX(&H21, XRegister, XRegister)
'if the carry flag is clear then the file exists
FileExists% = ((XRegister.Flags AND 1) = 0)
'restore the old DTA
XRegister.AX = &H1A00
XRegister.DS = OldDTASeg%
XRegister.DX = OldDTAOff%
CALL InterruptX(&H21, XRegister, XRegister)

END FUNCTION

FUNCTION FileLineCount% (FileName$)
lines% = 0
File = FREEFILE
OPEN FileName$ FOR INPUT AS File
  DO UNTIL EOF(File)
    lines% = lines% + 1
    LINE INPUT #File, FileLine$
  LOOP
CLOSE File
FileLineCount% = lines%
END FUNCTION

FUNCTION FileSize& (FileName$)
File = FREEFILE
OPEN FileName$ FOR BINARY AS File
  FileSize& = LOF(File)
CLOSE File
END FUNCTION

SUB Filter (Dirty$)

Char$ = CHR$(7): GOSUB FilterIt       'beep
BadChr$ = CHR$(12): GOSUB FilterIt    'Form feed

BadChr$ = CHR$(9)                     'tab
Ptr% = 1
DO
  Ptr% = INSTR(Ptr%, Dirty$, BadChr$)
  IF Ptr% THEN
    TabChars% = TabLen - (Ptr% MOD TabLen%)
    Dirty$ = MID$(Dirty$, 1, Ptr% - 1) + SPACE$(TabChars%) + MID$(Dirty$, Ptr% + 1)
  ELSE
    EXIT DO
  END IF
LOOP
Dirty$ = LEFT$(Dirty$, MaxChars%)
EXIT SUB

FilterIt:
Ptr% = 1
DO
  Ptr% = INSTR(Ptr%, Dirty$, Char$)
  IF Ptr% THEN
    MID$(Dirty$, Ptr%, 1) = " "
  ELSE
    EXIT DO
  END IF
LOOP
RETURN

END SUB

FUNCTION Greater% (Var1%, Var2%) STATIC
Greater% = Var1%: IF Var2% > Var1% THEN Greater% = Var2%
END FUNCTION

SUB InputBox (Prompt$, InputText$, KeyCode%)

COLOR BoxFG%, BoxBG%
CALL DrawBox(MidRow% - 1, 3, LeftCol% + 3, ScrnWidth% - 6)
DO
  LOCATE MidRow%, LeftCol% + 7: PRINT Prompt$;
  CALL InputLine(InputText$, ScrnWidth% - 15 - LEN(Prompt$), KeyCode%)
LOOP UNTIL KeyCode% = ENTER OR KeyCode% = ESC
COLOR FG%, BG%

END SUB

SUB InputLine (Text$, ViewWidth%, KeyCode%)

MaxLnLen% = 128
LCol% = POS(0): RCol% = LCol% + ViewWidth% - 1
View$ = SPACE$(ViewWidth%)
Work$ = LEFT$(Text$, MaxLnLen%)
LnLen% = LEN(Work$)
CCol% = Lesser(LCol% + LnLen%, RCol%)
AChar% = LnLen% + 1

LOCATE , , 0, CursSize% - 1, CursSize%
Insert% = True
DO
  LOCATE , LCol%
  LSET View$ = MID$(Work$, AChar% - (CCol% - LCol%), ViewWidth%)
  PRINT View$;
  LOCATE , CCol%, 1
  LnLen% = LEN(Work$)
  DO
    DO: Ky$ = INKEY$: LOOP UNTIL LEN(Ky$)
    KeyCode% = ASC(Ky$): IF KeyCode% = 0 THEN KeyCode% = -ASC(MID$(Ky$, 2))
  LOOP UNTIL KeyCode% <> BadKey%: BadKey% = 0
  LOCATE , , 0
  SELECT CASE KeyCode%
    CASE BkSpc
	 IF AChar% = 1 THEN
	   BEEP: BadKey% = KeyCode%
	 ELSE
	   IF AChar% <= LnLen% + 1 THEN
		Work$ = LEFT$(Work$, AChar% - 2) + MID$(Work$, AChar%)
	   END IF
	   AChar% = AChar% - 1
	   IF CCol% <> LCol% THEN CCol% = CCol% - 1
	 END IF
    CASE ENTER
	 Text$ = Work$
	 EXIT DO
    CASE ESC
	 EXIT DO
    CASE 32 TO 255      ' letter keys
	 IF AChar% > MaxLnLen% THEN
	   BEEP: BadKey% = KeyCode%
	 ELSE
	   IF AChar% <= LnLen% THEN
		IF Insert% THEN
		  IF LnLen% = MaxLnLen% THEN
		    BEEP: BadKey% = KeyCode%
		  ELSE
		    Work$ = LEFT$(Work$, AChar% - 1) + Ky$ + MID$(Work$, AChar%)
		    AChar% = AChar% + 1
		    IF CCol% <> RCol% THEN CCol% = CCol% + 1
		  END IF
		ELSE 'IF NOT Insert% THEN (in overstrike mode)
		  MID$(Work$, AChar%, 1) = Ky$
		  AChar% = AChar% + 1
		  IF CCol% <> RCol% THEN CCol% = CCol% + 1
		END IF
	   ELSE 'IF AChar% > LnLen% THEN
		Work$ = Work$ + SPACE$(AChar% - LnLen% - 1) + Ky$
		AChar% = AChar% + 1
		IF CCol% <> RCol% THEN CCol% = CCol% + 1
	   END IF
	 END IF
    CASE LArrow
	 IF AChar% = 1 THEN
	   BEEP: BadKey% = KeyCode%
	 ELSE 'IF AChar% > 1 THEN
	   AChar% = AChar% - 1
	   IF CCol% <> LCol% THEN CCol% = CCol% - 1
	 END IF
    CASE RArrow
	 IF AChar% > MaxLnLen% THEN
	   BEEP: BadKey% = KeyCode%
	 ELSE
	   AChar% = AChar% + 1
	   IF CCol% <> RCol% THEN CCol% = CCol% + 1
	 END IF
    CASE HomeKey
	 AChar% = 1
	 CCol% = LCol%
    CASE EndKey
	 AChar% = LnLen% + 1
	 CCol% = Lesser(LCol% + LnLen%, RCol%)
    CASE InsertKey
	 Insert% = NOT Insert%
	 IF Insert% THEN
	   LOCATE , , , CursSize% - 1, CursSize%
	 ELSE
	   LOCATE , , , CursSize% \ 2, CursSize%
	 END IF
    CASE DeleteKey
	 IF AChar% <= LnLen% THEN
	   Work$ = LEFT$(Work$, AChar% - 1) + MID$(Work$, AChar% + 1)
	 END IF
    CASE ELSE
	 BEEP: BadKey% = KeyCode%
  END SELECT
 LOOP
 LOCATE , , , CursSize% - 1, CursSize%

END SUB

 FUNCTION LegalFileName% (FileName$)
 DIM XRegister AS RegTypeX
 LegalFileName% = False
 IF FileExists%(FileName$) THEN
  LegalFileName% = True
 ELSE
  'attempt to create the file
  Temp$ = FileName$ + CHR$(0)
  XRegister.AX = &H3C00
  XRegister.CX = 0
  XRegister.DS = VARSEG(Temp$)
  XRegister.DX = SADD(Temp$)
  CALL InterruptX(&H21, XRegister, XRegister)
  IF (XRegister.Flags AND 1) = 0 THEN
    'if carry flag is clear, then file was created OK
    LegalFileName% = True
    'delete the file
    XRegister.AX = &H4100
    CALL InterruptX(&H21, XRegister, XRegister)
  END IF
 END IF

 END FUNCTION

 FUNCTION Lesser% (Var1%, Var2%) STATIC
 Lesser% = Var1%: IF Var2% < Var1% THEN Lesser% = Var2%
 END FUNCTION

SUB MsgBox (Msg$, ValidKeys$, KeyCode%)

 W% = LEN(Msg$) + 8
 M% = ScrnWidth% - 6
 IF W% > M% THEN W% = M% - 4
 D% = (ScrnWidth% - W%) \ 2
 COLOR BoxFG%, BoxBG%
 CALL DrawBox(MidRow% - 1, 3, LeftCol% + D%, ScrnWidth% - (D% * 2))
 LOCATE MidRow%, LeftCol% + D% + 4: PRINT LEFT$(Msg$, W%);
 COLOR FG%, BG%

 DO WHILE LEN(INKEY$): LOOP
 IF LEN(ValidKeys$) THEN
  LOCATE , , 1
  DO
    DO: VK$ = INKEY$: LOOP UNTIL LEN(VK$)
    IF ASC(VK$) = ESC THEN EXIT DO
    IF INSTR(ValidKeys$, VK$) THEN EXIT DO ELSE BEEP
  LOOP
  LOCATE , , 0
  KeyCode% = ASC(VK$)
 ELSE
  DO UNTIL LEN(INKEY$): LOOP
 END IF
END SUB

SUB SetVGABorderColor (Colour%)
 'Colour must be in the range 0 - 255
 DIM Register AS RegType
 Register.AX = &H1001
 Register.BX = Colour% * &H100
 CALL Interrupt(&H10, Register, Register)
END SUB

