        NAM   HDOS09
        TTL   COMMAND INTERFACE WITH HISTORY
**********************************************************
* HDOS09 is a command line interface with some improved  *
* editing capabilities.  It runs as an application that  *
* run commands via the DOCMND flex function.             *
* The program itself consist of a launcher that use the  *
* Flex MEMEND variable to put itself just before MEMEND  *
* address and move MEMEND from BFFF to BBFF-B7FF, given  *
* the size (175 to 1199 bytes) needed for the history.   *
* HSIZE allows to select the amount of memory used       *
* Prompt is modified to '$ ' (show you are using Hdos09) *
* -- New functions with control characters :             *
* ^B and ^F are used to move the cursor in the command   *
* line (Backward, Forward)                               *
* ^A and ^E move the cursor to start and end of line     *
* ^P (previous) and ^N (next) let you navigate history   *
* ^H (backspace) delete the character before the cursor  *
* ^X delete the character under the cursor               *
* Entering a printable character insert it before cursor *
* CR (Return) run the command, wherever the cursor is    *
* -- New commands :                                      *
*       h (or H) display history                         *
*       q (or Q) leave Hdos09, end restore MEMEND        *
*       !<number> runs the <number>th command in history *
*                                                        *
* (C) 2022 Michel J. Wurtz under GPL2 licence :          *
* https://opensource.org/licenses/GPL-2.0                *
**********************************************************

* Flex memory fields used
LINBUF  EQU   $C080    ; Line buffer start
TTYBS   EQU   $CC00    ; Backspace character
TTYDEL  EQU   $CC01    ; Delete caracter
TTYEOL  EQU   $CC02    ; End of line character
TTYBE   EQU   $CC07    ; Backspace echo charater
TTYESC  EQU   $CC0A    ; Escape character
LINPTR  EQU   $CC14    ; Line buffer pointer
MEMEND  EQU   $CC2B    ; End of user memory
SCRTCH  EQU   $CC2A    ; System scratch 
BUFLEN  EQU   $CC2A    ; = SCRTCH : length of current line
SCRTC3  EQU   $CC30    ; System scratch (3 bytes)
* Flex entry points used
WARMS   EQU   $CD03    ; Warm entry point
PSTRNG  EQU   $CD1E    ; Print string
INCHNE  EQU   $D3E5    ; Get char w/o echo (indirect)
PUTCHR  EQU   $CD18    ; print char in A
PCRLF   EQU   $CD24    ; print CR/LF
OUTDEC  EQU   $CD39    ; Output reg D in decimal
INDEC   EQU   $CD48    ; Input décimal number
DOCMND  EQU   $CD4B    ; run command in BUFLIN
* Constant used
CR      EQU   $0D      ; Cariage Return - terminate entry
LF      EQU   $0A      ; Line Feed
BK      EQU   $02      ; ^B move cursor backward
FW      EQU   $06      ; ^F move cursor forward
BL      EQU   $01      ; ^A move cursor to 1st character
EL      EQU   $05      ; ^E move cursor to end of line
PR      EQU   $10      ; ^P Previous command
NX      EQU   $0E      ; ^N Next command
BS      EQU   $08      ; ^H (Backspace) erase prev. char
DEL     EQU   $18      ; ^X erase current character
* Size of history + this utility (from $400 to $800)
HSIZE   EQU   $600     ; 687 bytes for history
* Loader : only used for moving program under MEMEND
        ORG   $A000
HSTART  LDX   MEMEND   ; Search start of hdos
        STX   HEND     ; Save it for futur use
        LEAX  -HSIZE,X ; New value of MEMEND
        STX   MEMEND
        LEAX  1,X
        LDY   #HINIT   ; code to move
HLOAD   LDA   ,Y+
        STA   ,X+
        CMPY  #HIST+2
        BNE   HLOAD
        LDX   MEMEND
        JMP   1,X      ; Jump to HINIT (moved to HEND+1)
*
* Relocatable part : goes to $B800 if HSIZE = $800
* This suppose a non modified system with MEMEND = $BFFF
*
*        ORG  $BFFF-HSIZE+1 ; uncomment for debug only !
HINIT   LEAU  HEND,PC
        LEAX  8,U      ; Clean history
        STX   2,U      ; Initialise pointers
        STX   4,U
        STX   6,U
        CLRA
CLEAN   STA   ,X+
        CMPX  ,U       ; HEND
        BNE   CLEAN

*
* Main DOS routine : prompt + read/edit a line
*
RDLINE  LEAU  HEND,PC  ; init pointer to hist data
        JSR   PCRLF    ; Output prompt
        LDA   #'$'
        JSR   PUTCHR
        LDA   #$20
        JSR   PUTCHR
        LDX   #LINBUF
        STX   LINPTR
        CLR   LINBUF
        CLR   LINBUF+1
        CLR   BUFLEN   ; start with emplty command
*
* Get a new char from keybord and manage it
*
GETC    JSR   [INCHNE] ; Main loop for input data
        CMPA  #CR      ; Carriage Return ?
        LBEQ  LAUNCH   ; Add to history and run command
*
* CTRL_B - Move cursor left (Backward)
*
        CMPA  #BK      ; ^B : move cursor left
        BNE   CTRL_F
        LDX   LINPTR
        CMPX  #LINBUF  ; if start of line do nothing
        BEQ   GETC
        LEAX  -1,X
        STX   LINPTR
        LDA   #BS
GETCE   JSR   PUTCHR
        BRA   GETC
*
* CTRL_F - Move cursor right (Forward)
*
CTRL_F  CMPA  #FW      ; ^F : move cursor right
        BNE   CTRL_A
        LDX   LINPTR
        LDA   ,X
        BEQ   GETC     ; If end of line do nothing
        LDA   ,X+      ; increment pointer and
        STX   LINPTR
        BRA   GETCE    ; print char in place
*
* CTRL_A - go to beginning of line
*
CTRL_A  CMPA  #BL      ; ^A move cursor to line start
        BNE   CTRL_E
        LDD   LINPTR
        SUBD  #LINBUF  ; calc length of move
        LDA   #BS
        TSTB
CTRA1   BEQ   CTRA2
        JSR   PUTCHR   ; move cursor
        DECB
        BRA   CTRA1
CTRA2   LDX   #LINBUF  ; adjust pointer
        STX   LINPTR
        BRA   GETC
*
* CTRL_E - go to end of line
*
CTRL_E  CMPA  #EL      ; ^E : move cursor to end of line
        BNE   TST_BS
        LDX   LINPTR
CTRE1   LDA   ,X+      ; reprint to end of line
        BEQ   CTRE2
        JSR   PUTCHR
        BRA   CTRE1
CTRE2   LEAX  -1,X     ; restore cursor position
        STX   LINPTR
        BRA   GETCE
*
* CTRL_H (BACKSPACE) - Erase previous character
*
TST_BS  CMPA  #BS      ; Backspace : erase previous char
        BNE   TSTDEL
        LDX   LINPTR
        CMPX  #LINBUF  ; if start of line do nothing
        BEQ   GETC
        LEAX  -1,X
        STX   LINPTR   ; update pointer
        LDA   #BS      ; move on screen
        JSR   PUTCHR
        BRA   MVLINE
*
* CTRL_X (DELETE) - Erase current character
*
TSTDEL  CMPA  #DEL     ; ^X : erase current char
        BNE   CTRL_P
        LDX   LINPTR
        LDA   ,X
        LBEQ  GETC     ; If end of line do nothing
MVLINE  DEC   BUFLEN   ; One char less
        CLRB
NXTC    LDA   1,X
        STA   ,X+
        BEQ   MVCUR
        JSR   PUTCHR   ; and reprint end of line
        INCB
        BRA   NXTC
MVCUR   STA   ,X
        LDA   #$20     ; erase last char with a space
        JSR   PUTCHR
        LDA   #BS      ; move cursor back
        INCB
MVCUR2  JSR   PUTCHR
        DECB
        BNE   MVCUR2
        LBRA  GETC
*
* CTRL_P - recall previous command
*
CTRL_P  CMPA  #PR      ; ^P : Previous command in history
        BNE   CTRL_N
        LDB   [6,U]    ; Current Command is the first ?
        LBEQ  GETC     ; Yes, do nothing
        NEGB
        SEX
        BMI   EXT
        LDA   #$FF
EXT     LDX   6,U      ; Calc. pos. of previous command
        LEAX  D,X
        LEAY  8,U
        STY   SCRTC3
        CMPX  SCRTC3   ; Wrap around end of hist. space ?
        BHS   NOWRAP
        TFR   X,D
        ADDD  ,U
        SUBD  SCRTC3
        TFR   D,X
        LEAX  1,X      ; correct displacement...
        LDB   1,X
NOWRAP  STX   6,U
        BRA   PUTCMD   ; go rewrite command line
*
* CTRL_N - recall next command
*
CTRL_N  CMPA  #NX      ; ^N - Next command in history
        BNE   NOCTRL
        LDX   6,U
        CMPX  4,U
        LBEQ  GETC     ; last command : do nothing
        LDA   ,X+
        LBSR  TSTWRP   ; history wrap around ?
        LDB   ,X+
        STB   BUFLEN   ; new command length
        ABX
        CMPX  ,U
        BLE   PUTCMD   ; go rewrite command line
        TFR   X,D
        SUBD  ,U
        LEAX  8,U
        LEAX  D,X
        LEAX  -1,X     ; correct displacement...
PUTCMD  STX   6,U      ; modify current command index
        LBSR  REPCMD   ; copy command on command line
        LBRA  GETC     ; wait for another keyboard input
*
* Printable char, add to or insert in command line
*
NOCTRL  CMPA  #$20     ; Printable char ?
        LBMI  GETC     ; Other ctrl character ignored
        LDB   BUFLEN
        CMPB  #127     ; Line too long ?
        LBEQ  GETC     ; Do nothing...
        LDX   #LINBUF  ; end of line ?
        ABX
        INC   BUFLEN   ; update buffer length
        CMPX  LINPTR
        BNE   INSRT    ; if not end of line ...
        JSR   PUTCHR   ; Add char to buffer and print it
        STA   ,X+
        STX   LINPTR
        CLR   ,X       ; keep end of string...
        LBRA  GETC
* Char entered must be inserted in command line
INSRT   STA   SCRTC3   ; Save char entered
        LDA   ,X
CPNXT   STA   1,X      ; move string right of cursor
        LDA   ,-X
        CMPX  LINPTR
        BNE   CPNXT
        STA   1,X
        LDA   SCRTC3   ; char entered
        STA   ,X+      ; put char entered at cursor pos.
        STX   LINPTR   ; and move cursor right
        CLRB
WRCH    JSR   PUTCHR   ; print char entered
        INCB           ; and move to end of line
        LDA   ,X+
        BNE   WRCH
        LDA   #BS      ; move cursor to its previous pos.
CPBS    DECB
        LBEQ  GETC
        JSR   PUTCHR   ; move cursor back until
        BRA   CPBS     ; cursor previous position
*
* CR - Final step : run command
*
LAUNCH  LDB   BUFLEN
        LBEQ  RDLINE   ; Empty command, go to prompt
        LDX   #LINBUF
        ABX
        STX   LINPTR
        STA   ,X
        LDA   LINBUF
        CMPB  #1
        BNE   RECALL
        ANDA  #$5F
        CMPA  #'H'     ; h or H - Print history
        BEQ   PRHIST
        CMPA  #'Q'     ; q or Q - quit interpreter
        BNE   RECALL
        LDX   ,U       ; restore MEMEND
        STX   MEMEND
        JMP   WARMS    ; Go to Flex warm start
RECALL  CMPA  #'!'     ; ! recall a command
        BNE   FLXCMD
        LBSR  BANG
        LBCS  RDLINE   ; No command found...
        LDA   #CR
        STA   [LINPTR]
FLXCMD  BSR   ADDCMD   ; Add command to history list
        LDX   #LINBUF
        STX   LINPTR   ; Run command
        JSR   DOCMND
        LBRA  RDLINE
*
* Command 'H' - Print command history
*
PRHIST  LDX   2,U
        LDD   -2,U     ; Initialise command number
        STD   SCRTC3
        LDB   ,X+
        BSR   TSTWRP
NXTCMD  LDB   ,X+
        BSR   TSTWRP
        TSTB
        LBEQ  RDLINE   ; last command ? return to prompt
        JSR   PCRLF
        BSR   PRCMD    ; Print command
        BRA   NXTCMD
*
* Print one command by line
*
PRCMD   PSHS  B,X      ; Save current length and position
        LDX   #SCRTC3  ; print command number
        JSR   OUTDEC
        LDX   SCRTC3   ; increment command number
        LEAX  1,X
        STX   SCRTC3
NONUM   LDA   #$20     ; space
        JSR   PUTCHR
        PULS  B,X
PRC1    LDA   ,X+      ; print command
        BSR   TSTWRP
PRC2    JSR   PUTCHR
        DECB
        BNE   PRC1
        LDB   ,X+
        BSR   TSTWRP   ; wap around history buffer
PRCE    RTS
*
* Wrap in history buffer ?
*
TSTWRP  CMPX  ,U       ; at MEMEND ?
        BLE   WRPOK
        LEAX  8,U      ; yes, go to HIST first byte
WRPOK   RTS
*
* Add command in history
*
ADDCMD  LDX   4,U      ; last command place
ADDC0   LEAX  1,X      ; Get next byte
        BSR   TSTWRP   ; hit MEMEND ?
        LDB   BUFLEN
        STB   ,X+      ; store command length (without CR)
        BSR   TSTWRP   ; hit MEMEND ?
        LDY   #LINBUF  ; Command to add
ADDC1   BSR   TSTHIT   ; Hit the beginning of list ?
        LDA   ,Y+
        STA   ,X+
        BSR   TSTWRP
        DECB
        BNE   ADDC1
        BSR   TSTHIT
        LDB   BUFLEN
        ADDB  #2       ; length of command + 2 (header)
        STX   4,U      ; set pointers for LAST and CURRENT
        STX   6,U
        STB   ,X+      ; header of last (future entry)
        BSR   TSTWRP
        BSR   TSTHIT
        CLR   ,X       ; mark end of new entry
        RTS
*
* Move FIRST pointer to next entry
*
*
* Start of history list reached when adding a command
* in history ? If yes, move first ptr to next entry
*
TSTHIT  PSHS  A,B,X
        CMPX  2,U      ; hit first command in history ?
        BNE   NOHIT    ; no, return
        LDA   ,X+
        BSR   TSTWRP
MVH2    LDB   ,X+
        ABX
        CMPX  ,U       ; past the end of HIST buffer ?
        BLE   MVOK     ; no, OK, go with it
        TFR   X,D      ; compute offset from HIST
        SUBD  ,U
        SUBD  #1
        LEAX  8,U
        LEAX  D,X
MVOK    STX   2,U      ; New value to FIRST
        CLR   ,X
        LDD   -2,U     ; to keep the number for commands
        ADDD  #1
        STD   -2,U
NOHIT   PULS  A,B,X,PC
*
* Copy command to line buffer, print it for edit
* X contains the current command address in memory
*
REPCMD  LDA   #CR      ; rewrite prompt at start of line
        JSR   PUTCHR
        LDA   #'$'
        JSR   PUTCHR
        LDA   #$20
        JSR   PUTCHR
        LDY   #LINBUF
        LDA   ,X+      ; skip link to previous
        BSR   TSTWRP   ; end of hist buffer reached ?
        LDB   ,X+
        STB   BUFLEN   ; save length of command
        BNE   NXTCC
        TFR   Y,X
        BRA   REPC1
NXTCC   LBSR  TSTWRP   ; copy to line buffer
        LDA   ,X+
        STA   ,Y+
        DECB
        BNE   NXTCC
        LDX   #LINBUF
        LDB   BUFLEN
NXPR    LDA   ,X+      ; print command
        JSR   PUTCHR
        DECB
        BNE   NXPR
REPC1   STX   LINPTR   ; update buffer pointer
        LDA   ,X
        BEQ   PREND    ; former cmd shorter than actual ?
PRSP    INCB
        CLRA
        STA   ,X+      ; terminate the command line
        LDA   #$20     ; clear end of line on screen
        JSR   PUTCHR
        LDA   ,X
        BNE   PRSP
        LDA   #BS      ; and move cursor to end of cmd
PRBS    JSR   PUTCHR
        DECB
        BNE   PRBS
PREND   RTS            ; (test => carry always clear)
*
* Command !N - run the Nth command
*
BANG    LDX   #LINBUF+1
        STX   LINPTR   ; get number
        JSR   INDEC
        BCS   ERRBG    ; not a decimal number 
        TSTB
        BEQ   ERRBG
        CMPX  -2,U     ; compare to first command number
        BLO   ERRBG    ; lost in wrap around
        STX   SCRTC3
        LDY   -2,U     ; Y containe current command number
        LDX   2,U      ; Oldest command
COMPAR  CMPY  SCRTC3
        BEQ   RUNIT    ; command found in history
        LDA   ,X+      ; no, go to next command
        LBSR  TSTWRP
        LDB   ,X+
        BEQ   ERRBG
        LBSR  TSTWRP
        LEAY  1,Y      ; increment command number
        ABX
        CMPX  ,U
        BLE   COMPAR   ; end of history ?
        TFR   X,D
        SUBD  ,U
        LEAX  8,U
        LEAX  D,X
        BRA   COMPAR   ; no, go to next command
RUNIT   LEAY  1,X      ; Last command is an empty slot
        CMPY  ,U       ; (length = 0) and not in history
        BLE   RUN2
        LEAY  8,U
RUN2    LDA   ,Y
        BEQ   ERRBG    ; hence go to error message
        LDA   #LF      ; command ok
        JSR   PUTCHR   ; skip line
        LDB   ,X       ; and print command
        LBRA  REPCMD   ; carry is clear on return...
* Command not found in history
ERRBG   LEAX  ERRMSG,PCR
        JSR   PSTRNG
        ORCC  #1       ; set carry if error
        RTS
ERRMSG  FCC   "Bad command number"
        FCB   4
*
* Data for history.  Must be accessed position independant
*
CMDNUM  FDB   1        ; First command number
HEND    FDB   0        ; MEMEND old val. = HIST buffer end
FIRST   FDB   0        ; First (oldest) command in history
LAST    FDB   0        ; Last command in history
CURRNT  FDB   0        ; Current command in history
HIST    FDB   0        ; History list (up to MEMEND)
