############################################################################
#    
#	File:     iolib.icn
#	
#	Subject:  Procedures for termlib support
#	
#	Author:   Richard L. Goerwitz (with help from Norman Azadian)
#
#	Date:     August 14, 1996
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#	Version:  1.13
#
############################################################################
#
#  The following library represents a series of rough functional
#  equivalents to the standard UNIX low-level termcap routines.  It is
#  not meant as an exact termlib clone.  Nor is it enhanced to take
#  care of magic cookie terminals, terminals that use \D in their
#  termcap entries, or archaic terminals that require padding.  This
#  library is geared mainly for use with ANSI and VT-100 devices.
#  Note that this file may, in most instances, be used in place of the
#  older UNIX-only itlib.icn file.  It essentially replaces the DOS-
#  only itlibdos routines.  For DOS users not familiar with the whole
#  notion of generalized screen I/O, I've included extra documentation
#  below.  Please read it.
#
#  The sole disadvantage of this over the old itlib routines is that
#  iolib.icn cannot deal with archaic or arcane UNIX terminals and/or
#  odd system file arrangements.  Note that because these routines
#  ignore padding, they can (unlike itlib.icn) be run on the NeXT and
#  other systems which fail to implement the -g option of the stty
#  command.  Iolib.icn is also simpler and faster than itlib.icn.
#
#  I want to thank Norman Azadian for suggesting the whole idea of
#  combining itlib.icn and itlibdos.icn into one distribution, for
#  suggesting things like letting drive specifications appear in DOS
#  TERMCAP environment variables, and for finding several bugs (e.g.
#  the lack of support for %2 and %3 in cm).  Although he is loathe
#  to accept this credit, I think he deserves it.
#
############################################################################
#
#  Contents:
#
#  setname(term)
#	Use only if you wish to initialize itermlib for a terminal
#  other than what your current environment specifies.  "Term" is the
#  name of the termcap entry to use.  Normally this initialization is
#  done automatically, and need not concern the user.
#
#  getval(id)
#	Works something like tgetnum, tgetflag, and tgetstr.  In the
#  spirit of Icon, all three have been collapsed into one routine.
#  Integer valued caps are returned as integers, strings as strings,
#  and flags as records (if a flag is set, then type(flag) will return
#  "true").  Absence of a given capability is signalled by procedure
#  failure.
#
#  igoto(cm,destcol,destline) - NB:  default 1 offset (*not* zero)!
#	Analogous to tgoto.  "Cm" is the cursor movement command for
#  the current terminal, as obtained via getval("cm").  Igoto()
#  returns a string which, when output via iputs, will cause the
#  cursor to move to column "destcol" and line "destline."  Column and
#  line are always calculated using a *one* offset.  This is far more
#  Iconish than the normal zero offset used by tgoto.  If you want to
#  go to the first square on your screen, then include in your program
#  "iputs(igoto(getval("cm"),1,1))."
#
#  iputs(cp,affcnt)
#	Equivalent to tputs.  "Cp" is a string obtained via getval(),
#  or, in the case of "cm," via igoto(getval("cm"),x,y).  Affcnt is a
#  count of affected lines.  It is completely irrelevant for most
#  modern terminals, and is supplied here merely for the sake of
#  backward compatibility with itlib, a UNIX-only version of these
#  routines (one which handles padding on archaic terminals).
#
############################################################################
#
#  Notes for MS-DOS users:
#
#	There are two basic reasons for using the I/O routines
#  contained in this package.  First, by using a set of generalized
#  routines, your code will become much more readable.  Secondly, by
#  using a high level interface, you can avoid the cardinal
#  programming error of hard coding things like screen length and
#  escape codes into your programs.
#
#	To use this collection of programs, you must do two things.
#  First, you must add the line "device=ansi.sys" (or the name of some
#  other driver, like zansi.sys, nansi.sys, or nnansi.sys [=new
#  nansi.sys]) to your config.sys file.  Secondly, you must add two
#  lines to your autoexec.bat file: 1) "set TERM=ansi-mono" and 2)
#  "set TERMCAP=\location\termcap."  The purpose of setting the TERM
#  variable is to tell this program what driver you are using.  If you
#  have a color system, you could use "ansi-color" instead of
#  "ansi-mono," although for compatibility with a broader range of
#  users, it would perhaps be better to stick with mono.  The purpose
#  of setting TERMCAP is to make it possible to determine where the
#  termcap database file is located.  The termcap file (which should
#  have been packed with this library as termcap.dos) is a short
#  database of all the escape sequences used by the various terminal
#  drivers.  Set TERMCAP so that it reflects the location of this file
#  (which should be renamed as termcap, for the sake of consistency
#  across UNIX and MS-DOS spectra).  If desired, you can also try
#  using termcap2.dos.  Certain games work a lot better using this
#  alternate file.  To try it out, rename it to termcap, and set
#  the environment variable TERMCAP to its location.
#
#	Although the authors make no pretense of providing here a
#  complete introduction to the format of the termcap database file,
#  it will be useful, we believe, to explain a few basic facts about
#  how to use this program in conjunction with it.  If, say, you want
#  to clear the screen, add the line,
#
#	iputs(getval("cl"))
#
#  to your program.  The function iputs() outputs screen control
#  sequences.  Getval retrieves a specific sequence from the termcap
#  file.  The string "cl" is the symbol used in the termcap file to
#  mark the code used to clear the screen.  By executing the
#  expression "iputs(getval("cl"))," you are 1) looking up the "cl"
#  (clear) code in the termcap database entry for your terminal, and
#  the 2) outputting that sequence to the screen.
#
#	Some other useful termcap symbols are "ce" (clear to end of
#  line), "ho" (go to the top left square on the screen), "so" (begin
#  standout mode), and "se" (end standout mode).  To output a
#  boldfaced string, str, to the screen, you would write -
#
#	iputs(getval("so"))
#	writes(str)
#	iputs(getval("se"))
#
#  You can also write "writes(getval("so") || str || getval("se")),
#  but this would make reimplementation for UNIX terminals that
#  require padding rather difficult.
#
#	It is also heartily to be recommended that MS-DOS programmers
#  try not to assume that everyone will be using a 25-line screen.
#  Most terminals are 24-line.  Some 43.  Some have variable window
#  sizes.  If you want to put a status line on, say, the 2nd-to-last
#  line of the screen, then determine what that line is by executing
#  "getval("li")."  The termcap database holds not only string-valued
#  sequences, but numeric ones as well.  The value of "li" tells you
#  how many lines the terminal has (compare "co," which will tell you
#  how many columns).  To go to the beginning of the second-to-last
#  line on the screen, type in:
#
#	iputs(igoto(getval("cm"), 1, getval("li")-1))
#
#  The "cm" capability is a special capability, and needs to be output
#  via igoto(cm,x,y), where cm is the sequence telling your computer
#  to move the cursor to a specified spot, x is the column, and y is
#  the row.  The expression "getval("li")-1" will return the number of
#  the second-to-last line on your screen.
#
############################################################################
#
#  Requires: UNIX or MS-DOS, co-expressions
#
############################################################################
#
#  See also: itlib.icn, iscreen.icn
#
############################################################################


global tc_table, isDOS
record true()


procedure check_features()

    initial {

	if find("UNIX",&features) then
	    isDOS := &null
	else if find("MS-DOS", &features) then
	    isDOS := 1
	else stop("check_features:  OS not (yet?) supported.")

	find("expressi",&features) |
	    er("check_features","co-expressions not implemented - &$#!",1)
    }

    return

end



procedure setname(name)

    # Sets current terminal type to "name" and builds a new termcap
    # capability database (residing in tc_table).  Fails if unable to
    # find a termcap entry for terminal type "name."  If you want it
    # to terminate with an error message under these circumstances,
    # comment out "| fail" below, and uncomment the er() line.

    #tc_table is global
    
    check_features()

    tc_table := table()
    tc_table := maketc_table(getentry(name)) | fail
    # er("setname","no termcap entry found for "||name,3)
    return "successfully reset for terminal " || name

end



procedure getname()

    # Getname() first checks to be sure we're running under DOS or
    # UNIX, and, if so, tries to figure out what the current terminal
    # type is, checking successively the value of the environment
    # variable TERM, and then (under UNIX) the output of "tset -".
    # Terminates with an error message if the terminal type cannot be
    # ascertained.  DOS defaults to "mono."

    local term, tset_output

    check_features()

    if \isDOS then {
        term := getenv("TERM") | "mono"
    }
    else {
	if not (term := getenv("TERM")) then {
	    tset_output := open("/bin/tset -","pr") |
		er("getname","can't find tset command",1)
	    term := !tset_output
	    close(tset_output)
	}
    }

    return \term |
	er("getname","can't seem to determine your terminal type",1)

end



procedure er(func,msg,errnum)

    # short error processing utility
    write(&errout,func,":  ",msg)
    exit(errnum)

end



procedure getentry(name, termcap_string)

    # "Name" designates the current terminal type.  Getentry() scans
    # the current environment for the variable TERMCAP.  If the
    # TERMCAP string represents a termcap entry for a terminal of type
    # "name," then getentry() returns the TERMCAP string.  Otherwise,
    # getentry() will check to see if TERMCAP is a file name.  If so,
    # getentry() will scan that file for an entry corresponding to
    # "name."  If the TERMCAP string does not designate a filename,
    # getentry() will scan the termcap file for the correct entry.
    # Whatever the input file, if an entry for terminal "name" is
    # found, getentry() returns that entry.  Otherwise, getentry()
    # fails.

    local isFILE, f, getline, line, nm, ent1, ent2, entry
    static slash, termcap_names
    initial {
	if \isDOS then {
	    slash := "\\"
	    termcap_names := ["termcap","termcap.dos","termcap2.dos"]
	}
	else {
	    slash := "/"
	    termcap_names := ["/etc/termcap"]
	}
    }


    # You can force getentry() to use a specific termcap file by cal-
    # ling it with a second argument - the name of the termcap file
    # to use instead of the regular one, or the one specified in the
    # termcap environment variable.
    /termcap_string := getenv("TERMCAP")

    if \isDOS then {
	if \termcap_string then {
	    if termcap_string ? (
		 not ((tab(any(&letters)), match(":")) | match(slash)),
		 pos(1) | tab(find("|")+1), =name)
	    then {
		# if entry ends in tc= then add in the named tc entry
		termcap_string ?:= tab(find("tc=")) ||
		    # Recursively fetch the new termcap entry w/ name trimmed.
		    # Note that on the next time through name won't match the
		    # termcap environment variable, so getentry() will look for
		    # a termcap file.
		    (move(3), getentry(tab(find(":"))) ?
		     (tab(find(":")+1), tab(0)))
		return termcap_string
	    }
	    else isFILE := 1
	}
    }
    else {
	if \termcap_string then {
	    if termcap_string ? (
	        not match(slash), pos(1) | tab(find("|")+1), =name)
	    then {
		# if entry ends in tc= then add in the named tc entry
		termcap_string ?:= tab(find("tc=")) ||
		    # Recursively fetch the new termcap entry w/ name trimmed.
		    (move(3), getentry(tab(find(":")), "/etc/termcap") ?
		     (tab(find(":")+1), tab(0)))
		return termcap_string
	    }
	    else isFILE := 1
	}
    }

    # The logic here probably isn't clear.  The idea is to try to use
    # the termcap environment variable successively as 1) a termcap en-
    # try and then 2) as a termcap file.  If neither works, 3) go to
    # the /etc/termcap file.  The else clause here does 2 and, if ne-
    # cessary, 3.  The "\termcap_string ? (not match..." expression
    # handles 1.

    if \isFILE			# if find(slash, \termcap_string)
    then f := open(\termcap_string)
    /f := open(!termcap_names) |
	er("getentry","I can't access your termcap file.  Read iolib.icn.",1)
    
    getline := create read_file(f)
    
    while line := @getline do {
	if line ? (pos(1) | tab(find("|")+1), =name, any(':|')) then {
	    entry := ""
	    while (\line | @getline) ? {
		if entry ||:= 1(tab(find(":")+1), pos(0))
		then {
		    close(f)
		    # if entry ends in tc= then add in the named tc entry
		    entry ?:= tab(find("tc=")) ||
		    # recursively fetch the new termcap entry
			(move(3), getentry(tab(find(":"))) ?
			 # remove the name field from the new entry
			 (tab(find(":")+1), tab(0)))
		    return entry
		}
		else {
		    \line := &null # must precede the next line
		    entry ||:= trim(trim(tab(0),'\\'),':')
		}
	    }
	}
    }

    close(f)
    er("getentry","can't find and/or process your termcap entry",3)
 
end



procedure read_file(f)

    # Suspends all non #-initial lines in the file f.
    # Removes leading tabs and spaces from lines before suspending
    # them.

    local line

    \f | er("read_tcap_file","no valid termcap file found",3)
    while line := read(f) do {
	match("#",line) & next
	line ?:= (tab(many('\t ')) | &null, tab(0))
	suspend line
    }

    fail

end



procedure maketc_table(entry)

    # Maketc_table(s) (where s is a valid termcap entry for some
    # terminal-type): Returns a table in which the keys are termcap
    # capability designators, and the values are the entries in
    # "entry" for those designators.

    local k, v, str, decoded_value

    /entry & er("maketc_table","no entry given",8)
    if entry[-1] ~== ":" then entry ||:= ":"
    
    /tc_table := table()

    entry ? {

	tab(find(":")+1)	# tab past initial (name) field

	while tab((find(":")+1) \ 1) ? {
	    &subject == "" & next
	    if k := 1(move(2), ="=") then {
		# Get rid of null padding information.  Iolib can't
		# handle it (unlike itlib.icn).  Leave star in.  It
		# indicates a real dinosaur terminal, and will later
		# prompt an abort.
		str := ="*" | ""; tab(many(&digits))
		decoded_value := Decode(str || tab(find(":")))
	    }
	    else if k := 1(move(2), ="#")
	    then decoded_value := integer(tab(find(":")))
	    else if k := 1(tab(find(":")), pos(-1))
	    then decoded_value := true()
	    else er("maketc_table", "your termcap file has a bad entry",3)
	    /tc_table[k] := decoded_value
	    &null
	}
    }

    return tc_table

end



procedure getval(id)

    /tc_table := maketc_table(getentry(getname())) |
	er("getval","can't make a table for your terminal",4)

    return \tc_table[id] | fail
	# er("getval","the current terminal doesn't support "||id,7)

end



procedure Decode(s)

    # Does things like turn ^ plus a letter into a genuine control
    # character.

    local new_s, chr, chr2

    new_s := ""

    s ? {

	while new_s ||:= tab(upto('\\^')) do {
	    chr := move(1)
	    if chr == "\\" then {
		new_s ||:= {
		    case chr2 := move(1) of {
			"\\" : "\\"
			"^"  : "^"
			"E"  : "\e"
			"b"  : "\b"
			"f"  : "\f"
			"n"  : "\n"
			"r"  : "\r"
			"t"  : "\t"
			default : {
			    if any(&digits,chr2) then {
				char(integer("8r"||chr2||move(2 to 0 by -1))) |
				    er("Decode","bad termcap entry",3)
			    }
			   else chr2
			}
		    }
		}
	    }
	    else new_s ||:= char(ord(map(move(1),&lcase,&ucase)) - 64)
	}
	new_s ||:= tab(0)
    }

    return new_s

end



procedure igoto(cm,col,line)

    local colline, range, increment, padding, str, outstr, chr, x, y

    if \col > (tc_table["co"]) | \line > (tc_table["li"]) then {
	colline := string(\col) || "," || string(\line) | string(\col|line)
	range := "(" || tc_table["co"]-1 || "," || tc_table["li"]-1 || ")"
	er("igoto",colline || " out of range " || (\range|""),9)
    } 

    # Use the Iconish 1;1 upper left corner & not the C-ish 0 offsets
    increment := -1
    outstr := ""
    
    cm ? {
	while outstr ||:= tab(find("%")) do {
	    tab(match("%"))
	    if padding := integer(tab(any('23')))
	    then chr := (="d" | "d")
	    else chr := move(1)
	    if case \chr of {
		"." :  outstr ||:= char(line + increment)
		"+" :  outstr ||:= char(line + ord(move(1)) + increment)
		"d" :  {
		    str := string(line + increment)
		    outstr ||:= right(str, \padding, "0") | str
		}
	    }
	    then line :=: col
	    else {
		case chr of {
		    "n" :  line := ixor(line,96) & col := ixor(col,96)
		    "i" :  increment := 0
		    "r" :  line :=: col
		    "%" :  outstr ||:= "%"
		    "B" :  line := ior(ishift(line / 10, 4), line % 10)
		    ">" :  {
			x := move(1); y := move(1)
			line > ord(x) & line +:= ord(y)
			&null
		    }
		} | er("goto","bad termcap entry",5)
	    }
	}
    return outstr || tab(0)
    }

end



procedure iputs(cp, affcnt)

    # Writes cp to the screen.  Use this instead of writes() for
    # compatibility with itlib (a UNIX-only version which can handle
    # albeit inelegantly) terminals that need padding.

    static num_chars
    initial num_chars := &digits ++ '.'

    type(cp) == "string" |
	er("iputs","you can't iputs() a non-string value!",10)

    cp ? {
	if tab(many(num_chars)) & ="*" then
	    stop("iputs:  iolib can't use terminals that require padding.")
	writes(tab(0))
    }

    return

end
