history: document ANSI escape sequences used
[pforth] / fth / history.fth
\ Command Line History
\
\ Author: Phil Burk
\ Copyright 1988 Phil Burk
\ Revised 2001 for pForth
0 [IF]
Requires an ANSI compatible terminal.
To get Windows computers to use ANSI mode in their DOS windows,
Add this line to "C:\CONFIG.SYS" then reboot.
device=c:\windows\command\ansi.sys
When command line history is on, you can use the UP and DOWN arrow to scroll
through previous commands. Use the LEFT and RIGHT arrows to edit within a line.
CONTROL-A moves to beginning of line.
CONTROL-E moves to end of line.
CONTROL-X erases entire line.
HISTORY# ( -- , dump history buffer with numbers)
HISTORY ( -- , dump history buffer )
XX ( line# -- , execute line x of history )
HISTORY.RESET ( -- , clear history tables )
HISTORY.ON ( -- , install history vectors )
HISTORY.OFF ( -- , uninstall history vectors )
[THEN]
include? ESC[ termio.fth
ANEW TASK-HISTORY.FTH
decimal
private{
\ You can expand the history buffer by increasing this constant!!!!!!!!!!
2048 constant KH_HISTORY_SIZE
create KH-HISTORY kh_history_size allot
KH-HISTORY kh_history_size erase
\ An entry in the history buffer consists of
\ byte - Count byte = N,
\ chars - N chars,
\ short - line number in Big Endian format,
\ byte - another Count byte = N, for reverse scan
\
\ The most recent entry is put at the beginning,
\ older entries are shifted up.
4 constant KH_LINE_EXTRA_SIZE ( 2 count bytes plus 2 line_number bytes )
: KH-END ( -- addr , end of history buffer )
kh-history kh_history_size +
;
: LINENUM@ ( addr -- w , stores in BigEndian format )
dup c@ 8 shift
swap 1+ c@ or
;
: LINENUM! ( w addr -- )
over -8 shift over c!
1+ c!
;
variable KH-LOOK ( cursor offset into history, point to 1st count byte of line )
variable KH-MAX
variable KH-COUNTER ( 16 bit counter for line # )
variable KH-SPAN ( total number of characters in line )
variable KH-MATCH-SPAN ( span for matching on shift-up )
variable KH-CURSOR ( points to next insertion point )
variable KH-ADDRESS ( address to store chars )
variable KH-INSIDE ( true if we are scrolling inside the history buffer )
: KH.MAKE.ROOM ( N -- , make room for N more bytes at beginning)
>r ( save N )
kh-history dup r@ + ( source dest )
kh_history_size r> - 0 max move
;
: KH.NEWEST.LINE ( -- addr count , most recent line )
kh-history count
;
: KH.REWIND ( -- , move cursor to most recent line )
0 kh-look !
;
: KH.CURRENT.ADDR ( -- $addr , count byte of current line )
kh-look @ kh-history +
;
: KH.CURRENT.LINE ( -- addr count )
kh.current.addr count
;
: KH.COMPARE ( addr count -- flag , true if redundant )
kh.newest.line compare 0= \ note: ANSI COMPARE is different than JForth days
;
: KH.NUM.ADDR ( -- addr , address of current line's line count )
kh.current.line +
;
: KH.CURRENT.NUM ( -- # , number of current line )
kh.num.addr LINENUM@
;
: KH.ADDR++ ( $addr -- $addr' , convert one kh to previous )
count + 3 +
;
: KH.ADDR-- ( $addr -- $addr' , convert one kh to next )
dup 1- c@ \ get next lines endcount
4 + \ account for lineNum and two count bytes
- \ calc previous address
;
: KH.ENDCOUNT.ADDR ( -- addr , address of current end count )
kh.num.addr 2+
;
: KH.ADD.LINE ( addr count -- )
dup 256 >
IF ." KH.ADD.LINE - Too big for history!" 2drop
ELSE ( add to end )
\ Compare with most recent line.
2dup kh.compare
IF 2drop
ELSE
>r ( save count )
\ Set look pointer to point to first count byte of last string.
0 kh-look !
\ Make room for this line of text and line header.
\ PLB20100823 Was cell+ which broke on 64-bit code.
r@ KH_LINE_EXTRA_SIZE + kh.make.room
\ Set count bytes at beginning and end.
r@ kh-history c! ( start count )
r@ kh.endcount.addr c!
kh-counter @ kh.num.addr LINENUM! ( line )
\ Number lines modulo 1024
kh-counter @ 1+ $ 3FF and kh-counter !
kh-history 1+ ( calc destination )
r> cmove ( copy chars into space )
THEN
THEN
;
: KH.BACKUP.LINE { | cantmove addr' -- cantmove , advance KH-LOOK if in bounds }
true -> cantmove ( default flag, at end of history )
\ KH-LOOK points to count at start of current line
kh.current.addr c@ \ do we have any lines?
IF
kh.current.addr kh.addr++ -> addr'
addr' kh-end U< \ within bounds?
IF
addr' c@ \ older line has chars?
IF
addr' kh-history - kh-look !
false -> cantmove
THEN
THEN
THEN
cantmove
;
: KH.FORWARD.LINE ( -- cantmove? )
kh-look @ 0= dup not
IF kh.current.addr kh.addr--
kh-history - kh-look !
THEN
;
: KH.OLDEST.LINE ( -- addr count | 0, oldest in buffer )
BEGIN kh.backup.line
UNTIL
kh.current.line dup 0=
IF
nip
THEN
;
: KH.FIND.LINE ( line# -- $addr )
kh.rewind
BEGIN kh.current.num over -
WHILE kh.backup.line
IF ." Line not in History Buffer!" cr drop 0 exit
THEN
REPEAT
drop kh.current.addr
;
: KH-BUFFER ( -- buffer )
kh-address @
;
: KH.RETURN ( -- , move to beginning of line )
0 out !
13 emit
;
: KH.REPLACE.LINE ( addr count -- , make this the current line of input )
kh.return
tio.erase.eol
dup kh-span !
dup kh-cursor !
2dup kh-buffer swap cmove
type
;
: KH.GET.MATCH ( -- , search for line with same start )
kh-match-span @ 0= ( keep length for multiple matches )
IF kh-span @ kh-match-span !
THEN
BEGIN
kh.backup.line not
WHILE
kh.current.line drop
kh-buffer kh-match-span @ text=
IF kh.current.line kh.replace.line
exit
THEN
REPEAT
;
: KH.FAR.RIGHT
kh-span @ kh-cursor @ - dup 0>
IF
tio.forwards
kh-span @ kh-cursor !
ELSE drop
THEN
;
: KH.FAR.LEFT ( -- )
kh.return
kh-cursor off
;
: KH.GET.OLDER ( -- , goto previous line )
kh-inside @
IF kh.backup.line drop
THEN
kh.current.line kh.replace.line
kh-inside on
;
: KH.GET.NEWER ( -- , next line )
kh.forward.line
IF
kh-inside off
tib 0
ELSE kh.current.line
THEN
kh.replace.line
;
: KH.CLEAR.LINE ( -- , rewind history scrolling and clear line )
kh.rewind
tib 0 kh.replace.line
kh-inside off
;
: KH.GO.RIGHT ( -- )
kh-cursor @ kh-span @ <
IF 1 kh-cursor +!
1 tio.forwards
THEN
;
: KH.GO.LEFT ( -- )
kh-cursor @ ?dup
IF 1- kh-cursor !
1 tio.backwards
THEN
;
: KH.REFRESH ( -- , redraw current line as is )
kh.return
kh-buffer kh-span @ type
tio.erase.eol
kh.return
kh-cursor @ ?dup
IF tio.forwards
THEN
kh-span @ out !
;
: KH.BACKSPACE ( -- , backspace character from buffer and screen )
kh-cursor @ ?dup ( past 0? )
IF kh-span @ <
IF ( inside line )
kh-buffer kh-cursor @ + ( -- source )
dup 1- ( -- source dest )
kh-span @ kh-cursor @ - cmove
\ ." Deleted!" cr
ELSE
backspace
THEN
-1 kh-span +!
-1 kh-cursor +!
ELSE bell
THEN
kh.refresh
;
: KH.DELETE ( -- , forward delete )
kh-cursor @ kh-span @ < ( before end )
IF ( inside line )
kh-buffer kh-cursor @ + 1+ ( -- source )
dup 1- ( -- source dest )
kh-span @ kh-cursor @ - 0 max cmove
-1 kh-span +!
kh.refresh
THEN
;
: KH.HANDLE.WINDOWS.KEY ( char -- , handle fkeys or arrows used by Windows ANSI.SYS )
CASE
$ 8D OF kh.get.match ENDOF
0 kh-match-span ! ( reset if any other key )
$ 48 OF kh.get.older ENDOF
$ 50 OF kh.get.newer ENDOF
$ 4D OF kh.go.right ENDOF
$ 4B OF kh.go.left ENDOF
$ 91 OF kh.clear.line ENDOF
$ 74 OF kh.far.right ENDOF
$ 73 OF kh.far.left ENDOF
$ 53 OF kh.delete ENDOF
ENDCASE
;
: KH.HANDLE.ANSI.KEY ( char -- , handle fkeys or arrows used by ANSI terminal )
CASE
$ 41 OF kh.get.older ENDOF
$ 42 OF kh.get.newer ENDOF
$ 43 OF kh.go.right ENDOF
$ 44 OF kh.go.left ENDOF
ENDCASE
;
: KH.SPECIAL.KEY ( char -- true | false , handle fkeys or arrows, true if handled )
true >r
CASE
$ E0 OF key kh.handle.windows.key
ENDOF
ASCII_ESCAPE OF
key dup $ 4F = \ for TELNET
$ 5B = OR \ for regular ANSI terminals
IF
key kh.handle.ansi.key
ELSE
rdrop false >r
THEN
ENDOF
ASCII_BACKSPACE OF kh.backspace ENDOF
ASCII_DELETE OF kh.backspace ENDOF
ASCII_CTRL_X OF kh.clear.line ENDOF
ASCII_CTRL_A OF kh.far.left ENDOF
ASCII_CTRL_E OF kh.far.right ENDOF
rdrop false >r
ENDCASE
r>
;
: KH.SMART.KEY ( -- char )
BEGIN
key dup kh.special.key
WHILE
drop
REPEAT
;
: KH.INSCHAR { charc | repaint -- }
false -> repaint
kh-cursor @ kh-span @ <
IF
\ Move characters up
kh-buffer kh-cursor @ + ( -- source )
dup 1+ ( -- source dest )
kh-span @ kh-cursor @ - cmove>
true -> repaint
THEN
\ write character to buffer
charc kh-buffer kh-cursor @ + c!
1 kh-cursor +!
1 kh-span +!
repaint
IF kh.refresh
ELSE charc emit
THEN
;
: EOL? ( char -- flag , true if an end of line character )
dup 13 =
swap 10 = OR
;
: KH.GETLINE ( max -- )
kh-max !
kh-span off
kh-cursor off
kh-inside off
kh.rewind
0 kh-match-span !
BEGIN
kh-max @ kh-span @ >
IF kh.smart.key
dup EOL? not ( <cr?> )
ELSE 0 false
THEN ( -- char flag )
WHILE ( -- char )
kh.inschar
REPEAT drop
kh-span @ kh-cursor @ - ?dup
IF tio.forwards ( move to end of line )
THEN
space
flushemit
;
: KH.ACCEPT ( addr max -- numChars )
swap kh-address !
kh.getline
kh-span @ 0>
IF kh-buffer kh-span @ kh.add.line
THEN
kh-span @
;
: TEST.HISTORY
4 0 DO
pad 128 kh.accept
cr pad swap type cr
LOOP
;
}private
: HISTORY# ( -- , dump history buffer with numbers)
cr kh.oldest.line ?dup
IF
BEGIN kh.current.num 3 .r ." ) " type ?pause cr
kh.forward.line 0=
WHILE kh.current.line
REPEAT
THEN
;
: HISTORY ( -- , dump history buffer )
cr kh.oldest.line ?dup
IF
BEGIN type ?pause cr
kh.forward.line 0=
WHILE kh.current.line
REPEAT
THEN
;
: XX ( line# -- , execute line x of history )
kh.find.line ?dup
IF count evaluate
THEN
;
: HISTORY.RESET ( -- , clear history tables )
kh-history kh_history_size erase
kh-counter off
;
: HISTORY.ON ( -- , install history vectors )
history.reset
what's accept ['] (accept) =
IF ['] kh.accept is accept
THEN
;
: HISTORY.OFF ( -- , uninstall history vectors )
what's accept ['] kh.accept =
IF ['] (accept) is accept
THEN
;
: AUTO.INIT
auto.init
history.on
;
: AUTO.TERM
history.off
auto.term
;
if.forgotten history.off
0 [IF]
history.reset
history.on
[THEN]