
##  General utilities using the kernel interface to the ncurses library.

BindGlobal("SplitWithEscapeSequences", function(str)
  local res, i, l, esc, j;
  res := [];
  i := 1;
  l := Length(str);
  esc := CHAR_INT(27);
  while i <= l do
    j := i;
    while j <= l and str[j] <> esc do
      j := j + 1;
    od;
    if j > i then
      Add(res, str{[i..j-1]});
      i := j;
    else
      # escape
      while j <= l and not str[j] in LETTERS do
        j := j + 1;
      od;
      Add(res, str{[i..j]});
      i := j+1;
    fi;
  od;
  return res;
end);


# need all of this only if kernel module was loaded 
if IsBound(NCurses) then


#############################################################################
##
#F  NCurses.IsAttributeLine( <obj> )
##
##  <#GAPDoc Label="IsAttributeLine_man">
##  <ManSection>
##  <Func Name="NCurses.IsAttributeLine" Arg="obj"/>
##
##  <Returns>
##  <K>true</K> if the argument describes a string with attributes.
##  </Returns>
##
##  <Description>
##  An <E>attribute line</E> describes a string with attributes.
##  It is represented by either a string or a dense list of strings,
##  integers, and Booleans immediately following integers,
##  where at least one list entry must <E>not</E> be a string.
##  (The reason is that we want to be able to distinguish between
##  an attribute line and a list of such lines,
##  and that the case of plain strings is perhaps the most usual one,
##  so we do not want to force wrapping each string in a list.)
##  The integers denote attribute values such as color or font information,
##  the Booleans denote that the attribute given by the preceding integer
##  is set or reset.
##  <P/>
##  If an integer is not followed by a Boolean then it is used as the attribute
##  for the following characters, that is it overwrites all previously set
##  attributes. Note that in some applications the variant with explicit 
##  Boolean values is preferable, because such a line can nicely be highlighted
##  just by prepending a <C>NCurses.attrs.STANDOUT</C> attribute.
##  <P/>
##  For an overview of attributes,
##  see&nbsp;<Ref Subsect="ssec:ncursesAttrs"/>.
##  <P/>
##  <Example><![CDATA[
##  gap> NCurses.IsAttributeLine( "abc" );
##  true
##  gap> NCurses.IsAttributeLine( [ "abc", "def" ] );
##  false
##  gap> NCurses.IsAttributeLine( [ NCurses.attrs.UNDERLINE, true, "abc" ] );
##  true
##  gap> NCurses.IsAttributeLine( "" );  NCurses.IsAttributeLine( [] );
##  true
##  false
##  ]]></Example>
##  <P/>
##  The <E>empty string</E> is an attribute line whereas the
##  <E>empty list</E> (which is not in <C>IsStringRep</C>,
##  see&nbsp;<Ref Func="IsStringRep" BookName="ref"/>)
##  is <E>not</E> an attribute line.
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
NCurses.IsAttributeLine :=
     obj -> IsStringRep( obj ) or
            ( IsDenseList( obj ) and
              ForAll( obj, x -> IsStringRep( x ) or IsInt( x )
                                                 or IsBool( x ) ) and
              ForAny( obj, x -> not IsStringRep( x ) ) );


#############################################################################
##
#F  NCurses.SimpleString( <line> )
##
##  For an attribute line <A>line</A>, <C>NCurses.SimpleString</C> returns
##  the string that is obtained by removing the attributes information from
##  <A>line</A>.
##
##  (Should this be documented?)
##
NCurses.SimpleString := function( line )
    if not IsString( line ) then
      line:= Concatenation( Filtered( line, IsString ) );
      if IsEmpty( line ) then
        line:= "";
      fi;
    fi;
    return line;
end;


#############################################################################
##
#F  NCurses.ConcatenationAttributeLines( <lines>[, <keep>] )
##
##  <#GAPDoc Label="ConcatenationAttributeLines_man">
##  <ManSection>
##  <Func Name="NCurses.ConcatenationAttributeLines" Arg="lines[, keep]"/>
##
##  <Returns>
##  an attribute line.
##  </Returns>
##
##  <Description>
##  For a list <A>lines</A> of attribute lines
##  (see <Ref Func="NCurses.IsAttributeLine"/>),
##  <C>NCurses.ConcatenationAttributeLines</C> returns the attribute line
##  obtained by concatenating the attribute lines in <A>lines</A>.
##  <P/>
##  If the optional argument <A>keep</A> is <K>true</K> then attributes set
##  in an entry of <A>lines</A> are valid also for the following entries
##  of <A>lines</A>.
##  Otherwise (in particular if there is no second argument) the attributes
##  are reset to <C>NCurses.attrs.NORMAL</C> between the entries of
##  <A>lines</A>.
##  <Example><![CDATA[
##  gap> plain_str:= "hello";;
##  gap> with_attr:= [ NCurses.attrs.BOLD, "bold" ];;
##  gap> NCurses.ConcatenationAttributeLines( [ plain_str, plain_str ] );
##  "hellohello"
##  gap> NCurses.ConcatenationAttributeLines( [ plain_str, with_attr ] );
##  [ "hello", 2097152, "bold" ]
##  gap> NCurses.ConcatenationAttributeLines( [ with_attr, plain_str ] );
##  [ 2097152, "bold", 0, "hello" ]
##  gap> NCurses.ConcatenationAttributeLines( [ with_attr, with_attr ] );
##  [ 2097152, "bold", 0, 2097152, "bold" ]
##  gap> NCurses.ConcatenationAttributeLines( [ with_attr, with_attr ], true );
##  [ 2097152, "bold", 2097152, "bold" ]
##  ]]></Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
NCurses.ConcatenationAttributeLines := function( arg )
    local lines, keep, result, line;

    lines:= arg[1];
    keep:= Length( arg ) = 2 and arg[2] = true;
    result:= "";
    for line in lines do
      if IsString( result ) then
        if IsString( line ) then
          Append( result, line );
        elif result = "" then
          result:= ShallowCopy( line );
        else
          result:= Concatenation( [ result ], line );
        fi;
      elif IsString( line ) then
        if keep then
          Add( result, line );
        else
          Append( result, [ NCurses.attrs.NORMAL, line ] );
        fi;
      else
        if not keep then
          Add( result, NCurses.attrs.NORMAL );
        fi;
        Append( result, line );
      fi;
    od;

    return result;
end;


#############################################################################
##
#F  NCurses.SublineAttributeLine( <line>, <from>, <len> )
##  
##  For an attribute line <A>line</A> and two positive integers <A>from</A>
##  and <A>len</A>,
##  <C>NCurses.SublineAttributeLine</C> returns the attribute line
##  that starts at the <A>from</A>-th displayed character of <A>line</A>
##  and is <A>len</A> displayed characters long.
##
##  (Should this be documented?)
##
NCurses.SublineAttributeLine:= function( line, from, len )
    local result, entry, elen;
    
    if IsString( line ) then
      result:= line{ [ from .. from + len - 1 ] };
    else
      result:= [];
      for entry in line do
        if IsString( entry ) then
          elen:= Length( entry );
          if elen < from then 
            from:= from - elen;
          else
            if from > 1 then
              entry:= entry{ [ from .. elen ] };
              elen:= elen - from + 1;
              from:= 1;
            fi;
            if elen <= len then
              Add( result, entry );
              len:= len - elen;
            else
              Add( result, entry{ [ 1 .. len ] } );
              len:= 0;
            fi;
          fi;
        else 
          Add( result, entry );
        fi;
      od;
    fi;
    return result;
end;


#############################################################################
##
#F  NCurses.RepeatedAttributeLine( <line>, <width> )
##
##  <#GAPDoc Label="RepeatedAttributeLine_man">
##  <ManSection>
##  <Func Name="NCurses.RepeatedAttributeLine" Arg="line, width"/>
##
##  <Returns>
##  an attribute line.
##  </Returns>
##
##  <Description>
##  For an attribute line <A>line</A>
##  (see <Ref Func="NCurses.IsAttributeLine"/>)
##  and a positive integer <A>width</A>,
##  <C>NCurses.RepeatedAttributeLine</C> returns an attribute line with
##  <A>width</A> displayed characters
##  (see&nbsp;<Ref Func="NCurses.WidthAttributeLine"/>)
##  that is obtained by concatenating sufficiently many copies of <A>line</A>
##  and cutting off a tail if applicable.
##  <P/>
##  <Example><![CDATA[
##  gap> NCurses.RepeatedAttributeLine( "12345", 23 );
##  "12345123451234512345123"
##  gap> NCurses.RepeatedAttributeLine( [ NCurses.attrs.BOLD, "12345" ], 13 );
##  [ 2097152, "12345", 0, 2097152, "12345", 0, 2097152, "123" ]
##  ]]></Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
NCurses.RepeatedAttributeLine:= function( line, width )
    local result, len, i;

    if width = 0 then
      return "";
    fi;
    len:= NCurses.WidthAttributeLine( line );
    if len = 0 then
      Error( "cannot get length <width> from empty line" );
    fi;

    if IsString( line ) then
      result:= RepeatedString( line, width );
    else
      result:= [];
      for i in [ 1 .. Int( width / len ) ] do
        Append( result, line );
        Add( result, NCurses.attrs.NORMAL );
      od;
      width:= width mod len;
      if width <> 0 then
        Append( result, NCurses.SublineAttributeLine( line, 1, width ) );
      fi;
    fi;
    return result;
end;


#############################################################################
##

##  For back translation of some Esc sequences to ncurses attributes.
NCurses.AttrEscSeq := 
rec( ("[0m") := NCurses.attrs.NORMAL, 
     ("[22m") := NCurses.attrs.NORMAL, 
     ("[1m") := NCurses.attrs.BOLD, 
     ("[4m") := NCurses.attrs.UNDERLINE, 
     ("[5m") := NCurses.attrs.BLINK, 
     ("[7m") := NCurses.attrs.REVERSE 
);
if NCurses.attrs.has_colors then
  # assume white background, only handle foreground colors
  NCurses.AttrEscSeq.("[30m") := NCurses.attrs.ColorPairs[56+0];
  NCurses.AttrEscSeq.("[31m") := NCurses.attrs.ColorPairs[56+1]; 
  NCurses.AttrEscSeq.("[32m") := NCurses.attrs.ColorPairs[56+2]; 
  NCurses.AttrEscSeq.("[33m") := NCurses.attrs.ColorPairs[56+3]; 
  NCurses.AttrEscSeq.("[34m") := NCurses.attrs.ColorPairs[56+4]; 
  NCurses.AttrEscSeq.("[35m") := NCurses.attrs.ColorPairs[56+5]; 
  NCurses.AttrEscSeq.("[36m") := NCurses.attrs.ColorPairs[56+6]; 
  NCurses.AttrEscSeq.("[37m") := NCurses.attrs.ColorPairs[56+7]; 
else
  # if no colors available then ignore
  NCurses.AttrEscSeq.("[30m") := NCurses.attrs.NORMAL;
  NCurses.AttrEscSeq.("[31m") := NCurses.attrs.NORMAL;
  NCurses.AttrEscSeq.("[32m") := NCurses.attrs.NORMAL;
  NCurses.AttrEscSeq.("[33m") := NCurses.attrs.NORMAL;
  NCurses.AttrEscSeq.("[34m") := NCurses.attrs.NORMAL;
  NCurses.AttrEscSeq.("[35m") := NCurses.attrs.NORMAL;
  NCurses.AttrEscSeq.("[36m") := NCurses.attrs.NORMAL;
  NCurses.AttrEscSeq.("[37m") := NCurses.attrs.NORMAL;
fi;

NCurses.AttributeLineFromEscape := function(str)
  local l, res, a, aa;
  l := SplitWithEscapeSequences(str);
  res := [];
  for a in l do
    if a[1] = '\033' then
      aa := a{[2..Length(a)]};
      if IsBound(NCurses.AttrEscSeq.(aa)) then
        Add(res, NCurses.AttrEscSeq.(aa));
        if NCurses.AttrEscSeq.(aa) <> NCurses.attrs.NORMAL then
          Add(res, true);
        fi;
      else
        Add(res, NCurses.attrs.NORMAL);
      fi;
    else
      Add(res, a);
    fi;
  od;
  return res;
end;

  
# we change OnBreak to first leave visual mode
OnBreakSavedByBrowse := OnBreak;
OnBreak := function()
  NCurses.endwin();
  OnBreakSavedByBrowse();
end;
 

##  <#GAPDoc Label="NCurses.SetTerm">
##  <ManSection>
##  <Func Name="NCurses.SetTerm" Arg="[record]"/>
##  
##  <Description>
##  This function provides a unified interface to the various terminal
##  setting  functions of <C>ncurses</C> listed in 
##  <Ref Subsect="ssec:ncursesTermset" />. 
##  The optional argument is a record with components which are assigned to
##  <K>true</K> or <K>false</K>. Recognised components are:
##  <C>cbreak</C>, <C>echo</C>, <C>nl</C>, <C>intrflush</C>, <C>leaveok</C>,
##  <C>scrollok</C>, <C>keypad</C>, <C>raw</C> (with the obvious meaning if
##  set to <K>true</K> or <K>false</K>, respectively).
##  <P/>
##  The default, if no argument is given,  is <C>rec(cbreak := true,
##               echo := false,
##               nl := false,
##               intrflush := false,
##               leaveok := true,
##               scrollok := false,
##               keypad := true)</C>.
##  (This is a useful setting for many applications.) If there is an 
##  argument <Arg>record</Arg>, then the given components overwrite the 
##  corresponding defaults.
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
##  Without argument this sets the terminal to
##  cbreak, noecho, nonl, intrflash to FALSE, leaveok to TRUE, 
##  scrollok to FALSE, keypad to TRUE 
##  Overwrite components with rec(cbreak := true/false, intrflush :=
##  true/false, ...) record as argument.  
NCurses.SetTerm := function(arg)
  local r, c;
  r := rec(cbreak := true,
             echo := false,
             nl := false,
             intrflush := false,
             leaveok := true,
             scrollok := false,
             keypad := true);
  if Length(arg) > 0 then
    for c in NamesOfComponents(arg[1]) do
      r.(c) := arg[1].(c);
    od;
  fi;
  for c in ["cbreak", "echo", "nl"] do
    if IsBound(r.(c)) then
      if r.(c) = true then
        NCurses.(c)();
      else
        NCurses.(Concatenation("no", c))();
      fi;
    fi;
  od;
  for c in ["intrflush", "scrollok", "leaveok", "keypad", "raw"] do
    if IsBound(r.(c)) then
      NCurses.(c)(0, r.(c));
    fi;
  od;
end;

##  args:  handler, data[, setterm]        
##  here 'setterm' is arg for SetTerm, empty rec() by default
NCurses.Loop := function(arg)
  local keybind, data, setterm, fin, c, a, i;
  keybind := List(arg[1], ShallowCopy);
  # normalize keys to lists of integers
  for a in keybind do
    if not IsList(a[1]) then
      a[1] := [a[1]];
    fi;
    for i in [1..Length(a[1])] do
      if IsChar(a[1][i]) then
        a[1][i] := INT_CHAR(a[1][i]);
      fi;
    od;
  od;
  data := arg[2];
  if Length(arg) > 2 then
    setterm := arg[3];
  else
    setterm := rec();
  fi;
  NCurses.werase(0);
  NCurses.savetty();
  NCurses.SetTerm(setterm);
  NCurses.wrefresh(0);
  fin := false;
  while not fin do
    c := NCurses.wgetch(0);
    for a in keybind do
      if c in a[1] then
        fin := a[2](data, c);
      fi;
    od;
    NCurses.wrefresh(0);
  od;
  NCurses.ResetCursor();
  NCurses.resetty();
  NCurses.endwin();
end;

##  A few more utilities:

# call waddnstr with string length as n
NCurses.waddstr := function(win, str)
  return NCurses.waddnstr(win, str, Length(str));
end;

##  <#GAPDoc Label="NCurses.SaveRestoreWin">
##  <ManSection>
##  <Func Name="NCurses.SaveWin" Arg="win"/>
##  <Func Name="NCurses.StringsSaveWin" Arg="cont"/>
##  <Func Name="NCurses.RestoreWin" Arg="win, cont"/>
##  <Func Name="NCurses.ShowSaveWin" Arg="cont"/>
##  
##  <Returns>
##  a &GAP; object describing the contents of a window.
##  </Returns>
##
##  <Description>
##  These functions can be used to save and restore the contents of
##  <C>ncurses</C> windows. <Ref Func="NCurses.SaveWin"/> returns a list
##  <C>[nrows, ncols, chars]</C> giving the number of rows, number of
##  columns, and a list of integers describing the content of window 
##  <Arg>win</Arg>. The integers in the latter contain the displayed
##  characters plus  the attributes for the display.
##  <P/>
##  The function <Ref Func="NCurses.StringsSaveWin"/> translates data
##  <Arg>cont</Arg> in  form of the
##  output of <Ref Func="NCurses.SaveWin"/> to a list of <C>nrows</C>
##  strings giving the text of the rows of the saved window, and ignoring 
##  the attributes. You can view the result with <Ref
##  Func="NCurses.Pager"/>.
##  <P/>
##  The argument <Arg>cont</Arg> for <Ref Func="NCurses.RestoreWin"/>
##  must be of the same format as the output of
##  <Ref Func="NCurses.SaveWin"/>.
##  The content of the saved window is copied to the window <Arg>win</Arg>,
##  starting from the top-left corner as much as it fits. 
##  <P/>
##  The utility <Ref Func="NCurses.ShowSaveWin"/> can be used to display the
##  output of <Ref Func="NCurses.SaveWin"/> (as much of the top-left corner as
##  fits on the screen).
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
#T Add a remark about using `NCurses.Pager' for redisplaying the output of
#T NCurses.SaveWin!
##  
##  NCurses.SaveWin saves the current content of a window. The return value
##  can be used with NCurses.RestoreWin to write the saved content in
##  a window (starting from top-left corner, as much as fits).
NCurses.SaveWin := function(win)
  local res, max, i, j;
  res := [];
  max := NCurses.getmaxyx(win);
  for i in [0..max[1]-1] do
    for j in [0..max[2]-1] do
      NCurses.wmove(win,i,j);
      Add(res, NCurses.winch(win));
    od;
  od;
  Add(max, res);
  return max;
end;
NCurses.StringsSaveWin := function(save)
  return List([1..save[1]], i-> String(List(save[3]{[1..save[2]]+(i-1)*save[2]},
                x-> CHAR_INT(x mod 256))));
end;
NCurses.RestoreWin := function(win, save)
  local max, i, j;
  max := NCurses.getmaxyx(win);
  if max[1] > save[1] then
    max[1] := save[1];
  fi;
  if max[2] > save[2] then
    max[2] := save[2];
  fi;
  for i in [0..max[1]-1] do
    for j in [0..max[2]-1] do
      NCurses.wmove(win,i,j);
      NCurses.waddch(win, save[3][i*save[2]+j+1]);
    od;
  od;
end;
NCurses.ShowSaveWin := function(save)
  local w, p, m;
  w := NCurses.newwin(save[1],save[2],0,0);
  if w = false then
    w := NCurses.newwin(0,0,0,0);
  fi;
  p := NCurses.new_panel(w);
  NCurses.RestoreWin(w, save);
  m := NCurses.curs_set(0);
  NCurses.update_panels();
  NCurses.doupdate();
  NCurses.wgetch(w);
  NCurses.curs_set(m);
  NCurses.endwin();
  NCurses.del_panel(p);
  NCurses.delwin(w);
end;


##  <#GAPDoc Label="NCurses.WBorder">
##  <ManSection >
##  <Func Arg="win[, chars]" Name="NCurses.WBorder" />
##  <Description>
##  This is a convenient interface to the <C>ncurses</C> function 
##  <C>wborder</C>. It draws a border around the window <Arg>win</Arg>. If 
##  no second argument is given the default line drawing characters are 
##  used, see <Ref Subsect="ssec:ncursesLines" />. 
##  Otherwise, <Arg>chars</Arg> must be a list of  &GAP; characters
##  or integers specifying characters, possibly with attributes.
##  If <Arg>chars</Arg> has length 8 the characters are used for the
##  left/right/top/bottom sides and
##  top-left/top-right/bottom-left/bottom-right corners. If <Arg>chars</Arg>
##  contains 2 characters the first is used for the sides and the second for
##  all corners. If <Arg>chars</Arg> contains just one character it is used
##  for all sides including the corners.
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
##  NCurses.wborder needs plain list of characters or integers, we support 
##  a wrapper. Also we allow to give 1 or 2 characters for the lines/corners
##  and expand them to the needed 8 characters.
NCurses.WBorder := function(arg)
  local win, chars;
  win := arg[1];
  if Length(arg) > 1 then
    chars := arg[2];
  else
    chars := true;
  fi;
  # useful for configurable applications
  if chars = false then 
    return true;
  fi;
  if IsStringRep(chars) then
    chars := List(chars, INT_CHAR);
  fi;
  if IsInt(chars) then
    chars := [chars];
  fi;
  if IsList(chars) then
    if Length(chars) = 1 then
      chars := List([1..8], i-> chars[1]);
    elif Length(chars) < 8 then
      chars := Concatenation(List([1..4], i-> chars[1]),
                             List([1..4], i-> chars[2]));
    fi;
  fi;
  return NCurses.wborder(win, chars);
end;


##  <#GAPDoc Label="NCurses.ColorAttr">
##  <ManSection>
##  <Func Name="NCurses.ColorAttr" Arg="fgcolor, bgcolor"/>
##  <Var Name="NCurses.attrs.has_colors" />
##
##  <Returns>an attribute for setting the foreground and background color 
##  to be used on a terminal window (it is a &GAP; integer).</Returns>
##  <Description>
##  The return value can be used like any other attribute as described in 
##  <Ref Subsect="ssec:ncursesAttrs" />. The arguments <Arg>fgcolor</Arg>
##  and <Arg>bgcolor</Arg> can be given as strings, allowed are those in
##  <C>[ "black", "red", "green", "yellow", "blue", "magenta", 
##  "cyan", "white" ]</C>. These are the default foreground colors 0 to 7 
##  on ANSI terminals. Alternatively, the numbers 0 to 7 can  be used
##  directly as arguments. 
##  <P/>
##  Note that terminals can be configured in a way
##  such that these named colors are not the colors which are actually 
##  displayed.  
##  <P/>
##  The variable <Ref Var="NCurses.attrs.has_colors"/>
##  <Index>colors, availability</Index> is set to <K>true</K> 
##  or <K>false</K> if the terminal supports colors or not, respectively.
##  If a terminal does not support colors then <Ref Func="NCurses.ColorAttr"
##    /> always returns <C>NCurses.attrs.NORMAL</C>.
##  <P/>
##  For an attribute setting the foreground color with the default 
##  background color of the terminal use <C>-1</C> as <Arg>bgcolor</Arg> or
##  the same as <Arg>fgcolor</Arg>.
##  
##  <Example><![CDATA[
##  gap> win := NCurses.newwin(0,0,0,0);; pan := NCurses.new_panel(win);;
##  gap> defc := NCurses.defaultColors;;
##  gap> NCurses.wmove(win, 0, 0);;
##  gap> for a in defc do for b in defc do
##  >      NCurses.wattrset(win, NCurses.ColorAttr(a, b));
##  >      NCurses.waddstr(win, Concatenation(a,"/",b,"\t"));
##  >    od; od; 
##  gap> NCurses.update_panels();; NCurses.doupdate();;
##  gap> NCurses.napms(5000); # show for 5 seconds
##  gap> NCurses.endwin();; NCurses.del_panel(pan);; NCurses.delwin(win);;
##  ]]></Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  get attribute for fg/bg color pair
NCurses.defaultColors := ["black", "red", "green", "yellow",
                          "blue", "magenta", "cyan", "white"];
NCurses.ColorAttr := function(fg, bg)
  local CP;
  if not NCurses.attrs.has_colors then
    return NCurses.attrs.NORMAL;
  else
    CP := NCurses.attrs.ColorPairs;
  fi;
  if IsString(fg) then 
    fg := Position(NCurses.defaultColors, fg);
    if fg=fail then
      fg := 0;
    else
      fg := fg-1;
    fi;
  fi;
  if IsString(bg) then 
    bg := Position(NCurses.defaultColors, bg);
    if bg=fail then
      bg := 7;
    else
      bg := bg-1;
    fi;
  fi;
  if bg = -1 and IsBound(CP[fg*8+fg]) then
    return CP[fg*8+fg];
  fi;
  if bg = 0 and fg = 0 then
    return 0;
  fi;
  if IsBound(CP) and IsBound(CP[bg*8+fg]) then
    return CP[bg*8+fg];
  fi;
  return NCurses.attrs.NORMAL;
end;

# lines with T-ends
NCurses.whlineUTL := function(win, n, type)
  local pos, R, l, r;
  R := NCurses.lineDraw;
  l := rec(U := R.ULCORNER, T := R.LTEE, L := R.LLCORNER);
  r := rec(U := R.URCORNER, T := R.RTEE, L := R.LRCORNER);
  pos := NCurses.getyx(win);
  if pos[2]+n > NCurses.getmaxyx(win)[2] then
    return false;
  fi;
  NCurses.waddch(win, l.(type));
  NCurses.whline(win, 0, n-2);
  NCurses.wmove(win, pos[1], pos[2]+n-1);
  NCurses.waddch(win, r.(type));
  return true;
end;

NCurses.whlineX := function(win, n)
  local pos, max, ch, i;
  pos := NCurses.getyx(win);
  max := NCurses.getmaxyx(win);
  for i in [1..Minimum(n, max[2]-pos[2])] do
    ch := NCurses.winch(win);
    if ch = NCurses.lineDraw.VLINE then
      NCurses.waddch(win, NCurses.lineDraw.PLUS);
    else
      NCurses.waddch(win, NCurses.lineDraw.HLINE);
    fi;
  od;
  NCurses.wmove(win, pos[1], pos[2]);
  return true;
end;

NCurses.wvlineLTR := function(win, n, type)
  local R, pos, u, l;
  R := NCurses.lineDraw;
  u := rec(L := R.ULCORNER, T := R.TTEE, R := R.URCORNER);
  l := rec(L := R.LLCORNER, T := R.BTEE, R := R.LRCORNER);
  pos := NCurses.getyx(win);
  if pos[1]+n > NCurses.getmaxyx(win)[1] then
    return false;
  fi;
  NCurses.waddch(win, u.(type));
  NCurses.wmove(win, pos[1]+1, pos[2]);
  NCurses.wvline(win, 0, n-2);
  NCurses.wmove(win, pos[1]+n-1, pos[2]);
  NCurses.waddch(win, l.(type));
  return true;
end;

NCurses.wvlineX := function(win, n)
  local pos, max, ch, i;
  pos := NCurses.getyx(win);
  max := NCurses.getmaxyx(win);
  for i in [1..Minimum(n, max[1]-pos[1])] do
    ch := NCurses.winch(win);
    if ch = NCurses.lineDraw.HLINE then
      NCurses.waddch(win, NCurses.lineDraw.PLUS);
    else
      NCurses.waddch(win, NCurses.lineDraw.VLINE);
    fi;
    NCurses.wmove(win, pos[1]+i, pos[2]);
  od;
  NCurses.wmove(win, pos[1], pos[2]);
  return true;
end;

##  <#GAPDoc Label="NCurses.Grid">
##  <ManSection >
##  <Func Arg="win, trow, brow, lcol, rcol, rowinds, colinds" 
##  Name="NCurses.Grid" />
##  <Description>
##  This function draws a grid of horizontal and vertical lines on the
##  window <Arg>win</Arg>, using the line drawing characters explained 
##  in <Ref Subsect="ssec:ncursesLines" />. The given arguments specify
##  the top and bottom row of the grid, its left and right column, and 
##  lists of row and column numbers where lines should be drawn. 
##  <Example><![CDATA[
##  gap> fun := function() local win, pan;
##  >      win := NCurses.newwin(0,0,0,0);
##  >      pan := NCurses.new_panel(win);
##  >      NCurses.Grid(win, 2, 11, 5, 22, [5, 6], [13, 14]);
##  >      NCurses.PutLine(win, 12, 0, "Press <Enter> to quit");
##  >      NCurses.update_panels(); NCurses.doupdate();
##  >      NCurses.wgetch(win);
##  >      NCurses.endwin();
##  >      NCurses.del_panel(pan); NCurses.delwin(win);
##  > end;;
##  gap> fun();
##  ]]></Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
NCurses.Grid := function(win, trow, brow, lcol, rcol, rowinds, colinds)
  local size, tvis, bvis, ht, lvis, rvis, wdth, ld, lmr, i, j;
  size := NCurses.getmaxyx(win);
  if size = false then return false; fi;
  if not ForAll([trow, brow, lcol, rcol], IsInt) then return false; fi;
  if not ForAll(rowinds, IsInt) then return false; fi;
  if not ForAll(colinds, IsInt) then return false; fi;
  # find viewable rows and cols
  rowinds := Filtered(rowinds, i-> i >= 0 and i >= trow and 
                                   i <= size[1]-1 and i <= brow);
  colinds := Filtered(colinds, i-> i >= 0 and i >= lcol and 
                                   i <= size[2]-1 and i <= rcol);
  tvis := Maximum(trow, 0);
  bvis := Minimum(brow, size[1]);
  ht := bvis - tvis + 1;
  lvis := Maximum(lcol, 0);
  rvis := Minimum(rcol, size[2]);
  wdth := rvis - lvis + 1;
  ld := NCurses.lineDraw;
  # draw vlines
  for i in colinds do
    NCurses.wmove(win, tvis, i);
    NCurses.wvline(win, ld.VLINE, ht);
  od;
  # draw hlines and handle crossings
  for i in rowinds do
    NCurses.wmove(win, i, lvis);
    NCurses.whline(win, ld.HLINE, wdth);
    if i = trow then
      lmr := [ld.ULCORNER, ld.TTEE, ld.URCORNER];
    elif i = brow then
      lmr := [ld.LLCORNER, ld.BTEE, ld.LRCORNER];
    else
      lmr := [ld.LTEE, ld.PLUS, ld.RTEE];
    fi;
    for j in colinds do
      NCurses.wmove(win, i, j);
      if j = lcol then
        NCurses.waddch(win, lmr[1]);
      elif j = rcol then
        NCurses.waddch(win, lmr[3]);
      else
        NCurses.waddch(win, lmr[2]);
      fi;
    od;
  od;
  return true;
end;


##  
##  Here, an 'attribute line' has the following format: it is a list  of GAP
##  integers, Booleans and strings.
##  
##  An integer describes an attribute  for the following terminal characters
##  (e.g., numbers or sums of numbers from NCurses.attrs).
##  
##  If an integer is followed by 'true', this attribute is switched 'on', in
##  addition to the attributes already set.
##  
##  If an integer is followed by  'false', this attribute is switched 'off',
##  the remaining attributes are left as before.
##  
##  Otherwise, the integer   is used as a whole set  of attributes which are
##  'set'.
#T  TB: ``i.e., the current attributes are cleaned and replaced by
#T      the new attribute set'' ?
##  
##  A string is  written to the terminal  as long as it fits  on the current
##  line.
##  
##  As  first  and last  step  the  current  attributes  are always  set  to
##  NCurses.attrs.NORMAL.
##  

##  <#GAPDoc Label="NCurses.WidthAttributeLine">
##  <ManSection>
##  <Func Name="NCurses.WidthAttributeLine" Arg="line"/>
##  <Returns>number of displayed characters in an attribute line.</Returns>
##  <Description>
##  For an attribute line <A>line</A>
##  (see <Ref Func="NCurses.IsAttributeLine"/>),
##  the function returns the number of displayed characters of <A>line</A>.
##  <Index>displayed characters</Index>
##  <P/>
##  <Example><![CDATA[
##  gap> NCurses.WidthAttributeLine( "abcde" );
##  5
##  gap> NCurses.WidthAttributeLine( [ NCurses.attrs.BOLD, "abc",
##  >        NCurses.attrs.NORMAL, "de" ] );
##  5
##  ]]></Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
NCurses.WidthAttributeLine := function(lstr)
  local res, a;
  if IsString(lstr) then
    return Length(lstr);
  fi;
  res := 0;
  for a in lstr do
    if IsString(a) then
      res := res + Length(a);
    fi;
  od;
  return res;
end;


##  <#GAPDoc Label="NCurses.PutLine">
##  <ManSection>
##  <Func Name="NCurses.PutLine" Arg="win, y, x, lines[, skip]"/>
##
##  <Returns>
##  <K>true</K> if <A>lines</A> were written, otherwise <K>false</K>.
##  </Returns>
##
##  <Description>
##  The argument <Arg>lines</Arg> can be a list of attribute lines (see 
##  <Ref Func="NCurses.IsAttributeLine" />) or a single attribute line.
##  This function  writes the attribute lines to window 
##  <Arg>win</Arg> at and below of position <Arg>y</Arg>, <Arg>x</Arg>.
##  <P/>
##  If the argument <Arg>skip</Arg> is given, it must be a nonnegative
##  integer. In that
##  case the first <Arg>skip</Arg> characters of each given line are not
##  written to the window (but the attributes are). 
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
# args:   win, y, x, lstr[, skip]          
NCurses.PutLine := function(arg)
  local win, y, x, lstr, skip, n, max, norm, i, s;
  win := arg[1]; y := arg[2]; x := arg[3]; lstr := arg[4];
  if Length(arg) > 4 then
    skip := arg[5];
  else
    skip := 0;
  fi;
  # handle lstr as string and list of attribute lines
  if IsString(lstr) then
    lstr := [lstr];
  elif IsList(lstr) and ForAll(lstr, NCurses.IsAttributeLine) then
    s := true;
    max := NCurses.getmaxyx(win);
    for i in [1..Length(lstr)] do
      if y+i-1 < max[1] then
        s := s and NCurses.PutLine(win, y+i-1, x, lstr[i], skip);
      fi;
    od;
    return s;
  elif not NCurses.IsAttributeLine(lstr) then
    return false;
  fi;
  # now the work begins
  n := Length(lstr);
  max := NCurses.getmaxyx(win);
  norm := NCurses.attrs.NORMAL;
  # we first reset all attributes
  NCurses.wattrset(win, norm);
  if max = false then return false; fi;
  if not NCurses.wmove(win, y, x) then return false; fi;
  # now we know maximal number of chars to be written
  max := max[2]-x;
  i := 1;
  while i <= n do
    if max <= 0 then break; fi;
    if IsInt(lstr[i]) then
      if i < n and lstr[i+1] = true then
        if not NCurses.wattron(win, lstr[i]) then
          NCurses.wattrset(win, norm);
          return false;
        else
          i := i+2;
        fi;
      elif i < n and lstr[i+1] = false then
        if not NCurses.wattroff(win, lstr[i]) then
          NCurses.wattrset(win, norm);
          return false;
        else
          i := i+2;
        fi;
      else
        if not NCurses.wattrset(win, lstr[i]) then
          NCurses.wattrset(win, norm);
          return false;
        else
          i := i+1;
        fi;
      fi;
    elif IsString(lstr[i]) then
      if not IsStringRep(lstr[i]) then
        # we need a C-string on kernel level
        s := ShallowCopy(lstr[i]);
        ConvertToStringRep(s);
      else
        s := lstr[i];
      fi;
      if skip > 0 then
        if skip > Length(s) then
          skip := skip - Length(s);
          s := "";
        else
          s := s{[skip+1..Length(s)]};
          skip := 0;
        fi;
      fi;
      if Length(s) > max then
        s := s{[1..max]};
      fi;
      if not NCurses.waddstr(win, s) then
        NCurses.wattrset(win, norm);
        return false;
      else
        max := max - Length(s);
        i := i+1;
      fi;
    else
      Error("Non-valid entry in attribute line: ", lstr[i], 
            ", 'return' to ignore.");
      i := i+1;
    fi;
  od;
  NCurses.wattrset(win, norm);
  return true;
end;


##  <#GAPDoc Label="NCurses.GetLineFromUser">
##  <ManSection>
##  <Func Name="NCurses.GetLineFromUser" Arg="pre"/>
##  <Returns>User input as string.</Returns>
##  <Description>
##  This function can be used to get an input string from the user. It opens a
##  one line window and writes the given string <Arg>pre</Arg> into it. Then it
##  waits for user input. After hitting the <B>Return</B> key the typed line is
##  returned as a string to &GAP;.
##  If the user exits via hitting the <B>Esc</B> key instead of hitting
##  the <B>Return</B> key,
##  the function returns <K>false</K>.
##  (The <B>Esc</B> key may be recognized as input only after a delay of about
##  a second.)
##  <P/>
##  Some simple editing is possible during user input: The <B>Left</B>,
##  <B>Right</B>, <B>Home</B> and <B>End</B> keys,
##  the <B>Insert</B>/<B>Replace</B> keys,
##  and the <B>Backspace</B>/<B>Delete</B> keys are supported.
##  <P/>
##  Instead of a string, <A>pre</A> can also be a record with the component
##  <C>prefix</C>, whose value is the string described above.
##  The following optional components of this record are supported.
##  <P/>
##  <List>
##  <Mark><C>window</C></Mark>
##  <Item>
##    The window with the input field is created relative to this window,
##    the default is <M>0</M>.
##  </Item>
##  <Mark><C>begin</C></Mark>
##  <Item>
##    This is a list with the coordinates of the upper left corner of the
##    window with the input field, relative to the window described by the
##    <C>window</C> component; the default is <C>[ y-4, 2 ]</C>,
##    where <C>y</C> is the height of this window.
##  </Item>
##  <Mark><C>default</C></Mark>
##  <Item>
##    This string appears as result when the window is opened,
##    the default is an empty string.
##  </Item>
##  </List>
##  <P/>
##  <Example><![CDATA[
##  gap> str := NCurses.GetLineFromUser("Your Name: ");;
##  gap> Print("Hello ", str, "!\n");
##  ]]></Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
# the curses wgetnstr seems strange with respect to left arrow/delete/
# backspace, so we write our own function
NCurses.GetLineFromUser := function( arec )
  local win, res, yx, begin, pan, off, max, pos, ins, c, curs;

  if not IsRecord( arec ) then
    arec:= rec( prefix:= arec );
  fi;
  win:= 0;
  if IsBound( arec.window ) then
    win:= arec.window;
  fi;
  res := "";
  if IsBound( arec.default ) then
    res:= arec.default;
  fi;
  yx := NCurses.getmaxyx( win );
  begin:= [ yx[1]-4, 2 ];
  if IsBound( arec.begin ) then
    begin:= arec.begin;
  fi;
  win := NCurses.newwin( 3, yx[2]-4, begin[1], begin[2] );
  pan := NCurses.new_panel(win);
  off := Length( arec.prefix );
  max := yx[2] - 6 - off;
  pos := 1;
  ins := true;
  NCurses.savetty();
  NCurses.SetTerm();
  # make sure cursor is visible
  curs := NCurses.curs_set(1);
  repeat
    # draw
    NCurses.werase(win);
    NCurses.PutLine(win, 1, 1, [NCurses.attrs.BOLD, arec.prefix,
                    NCurses.attrs.NORMAL, res]);
    NCurses.wborder(win, 0);
    NCurses.wmove(win, 1, off + pos);
    NCurses.update_panels();
    NCurses.doupdate();
    # get character and adjust
    c := NCurses.wgetch(win);
    if c = NCurses.keys.RIGHT then
      if pos <= Length(res) and pos < max then
        pos := pos + 1;
      fi;
    elif c = NCurses.keys.LEFT then
      if pos > 1 then
        pos := pos - 1;
      fi;
    elif c = NCurses.keys.IC then
      ins := not ins;
    elif c = NCurses.keys.REPLACE then
      ins := not ins;
    elif c in [NCurses.keys.HOME, INT_CHAR('')] then
      pos := 1;
    elif c in [NCurses.keys.END, INT_CHAR('')] then
      pos := Length(res) + 1;
      if pos > max then
        pos := pos - 1;
      fi;
    elif c in [NCurses.keys.BACKSPACE, INT_CHAR('')] then
      if pos > 1 then
        pos := pos - 1;
        RemoveElmList(res, pos);
      fi;
    elif c in [NCurses.keys.DC, INT_CHAR('')] then
      if pos <= Length(res) then
        RemoveElmList(res, pos);
      fi;
    elif not c in [ NCurses.keys.ENTER, INT_CHAR('
'), 27 ] then
      if ins and Length(res) < max then
        InsertElmList(res, pos, CHAR_INT(c mod 256));
        pos := pos + 1;
      elif not ins and pos <= max then
        res[pos] := CHAR_INT(c mod 256);
        pos := pos + 1;
      fi;
    fi;
  until c in [ NCurses.keys.ENTER, INT_CHAR('
'), 27 ];
  NCurses.del_panel(pan);
  NCurses.delwin(win);
  NCurses.curs_set(curs);
  NCurses.resetty();
  NCurses.endwin();

  if c = 27 then
    res:= false;
  fi;
## If this was called from visual mode, one should now say:  
##    NCurses.update_panels();
##    NCurses.doupdate();
  return res;
end;


##  <#GAPDoc Label="NCurses.Pager">
##  <ManSection>
##  <Func Name="NCurses.Pager" Arg="lines[,border[,ly, lx, y, x]]"/>
##  <Description>
##  This is a simple pager utility for displaying and scrolling text.
##  The argument <Arg>lines</Arg> can be a list of attribute lines (see
##  <Ref Func="NCurses.IsAttributeLine"/>) or a string (the lines are
##  separated by newline characters) or a record. In case of a record the 
##  following components are recognized:
##  <P/>
##  More details can be given to the function by using a record as argument
##  <Arg>poss</Arg>. It can have the following components:
##  <List >
##  <Mark><C>lines</C></Mark>
##  <Item>The list of attribute lines or a string as described above.</Item>
##  <Mark><C>start</C></Mark>
##  <Item>Line number to start the display.</Item>
##  <Mark><C>size</C></Mark>
##  <Item>The size <C>[ly, lx]</C> of the window like the first two arguments
##  of <C>NCurses.newwin</C> (default is <C>[0, 0]</C>, as big as possible).
##  </Item>
##  <Mark><C>begin</C></Mark>
##  <Item>Top-left corner <C>[y, x]</C> of the window like the last two
##  arguments of <C>NCurses.newwin</C>
##  (default is <C>[0, 0]</C>, top-left of the screen).
##  </Item>
##  <Mark><C>attribute</C></Mark>
##  <Item>An attribute used for the display of the window (default is
##  <C>NCurses.attrs.NORMAL</C>).</Item>
##  <Mark><C>border</C></Mark>
##  <Item>Either one of <K>true</K>/<K>false</K> to show the pager window 
##  with or without a standard border. Or it can be string with eight, two 
##  or one characters, giving characters to be used for a border, see
##  <Ref Func="NCurses.WBorder"/>.</Item>
##  <Mark><C>hint</C></Mark>
##  <Item> A text for usage info in the last line of the window.</Item>
##  </List>
##  <P/>
##  As an abbreviation the information from <C>border</C>, <C>size</C> and
##  <C>begin</C> can also be specified in optional arguments.
##  
##  <Example><![CDATA[
##  gap> lines := List([1..100],i-> ["line ",NCurses.attrs.BOLD,String(i)]);;
##  gap> NCurses.Pager(lines);
##  ]]></Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  Here 'lines' can be
##        - a list of attribute lines or strings
##        - a single string - it is then split into lines
##        - a record r with component .lines as above
##             r can have more optional components:
##                .start    line number to start display
##                .hint     for usage info in last line of window, default is
##                          "  [q quit, arrows to scroll, h help] "
##                .attribute    a global attribute for the pager window,
##                              default is none
##                .border       true/false for standard borders or not,
##                              or 8 characters for l,r,t,b,tl,tr,bl, br
##                              or 2 characters x,y as shortcut x,x,x,x,y,y,y,y
##                              or 1 character z as shortcut for z, z,
##                              default is false
##                .size         [ly, lx], default 0, 0 (whole screen)
##                .begin        [y, x] top left corner, default [0, 0]
##  
# args:  lines[, border, [ly, lx, y, x] ]
NCurses.Pager := function(arg)
  local r, sout, win, size, pan, off, pos, len, width, skip, nk, 
        ic, max, c, i, helppage, b;
  r := arg[1];
  if not IsRecord(r) then
    r := rec( lines := r );
  fi;
  if IsString(r.lines) then
    r.lines := SplitString(r.lines, "\n", "");
  fi;
  if Length(arg) > 1 then
    r.border := arg[2];
  fi;
  if not IsBound(r.border) then
    r.border := false;
  fi;
  if Length(arg) > 5 then
    r.size := [arg[3], arg[4]];
    r.begin := [arg[5], arg[6]];
  fi;
  if not IsBound(r.size) then
    r.size := [0, 0];
  fi;
  if not IsBound(r.begin) then
    r.begin := [0, 0];
  fi;
  sout := NCurses.attrs.STANDOUT;
  if not IsBound(r.hint) then
    r.hint := [sout, " [q quit, arrows to scroll, h help] "];
  fi;
  win := NCurses.newwin(r.size[1], r.size[2], r.begin[1], r.begin[2]);
  if win = false then
    win := NCurses.newwin(r.size[1], r.size[2], 0, 0);
  fi;
  if win = false then
    win := NCurses.newwin(0, 0, 0, 0);
  fi;
  if win = false then
    return false;
  fi;
  if IsBound(r.attribute) then
    NCurses.wbkgdset(win, r.attribute);
  fi;
  size := NCurses.getmaxyx(win);
  pan := NCurses.new_panel(win);
  if r.border <> false then
    off := 1;
  else
    off := 0;
  fi;
  len := Length(r.lines);
  if IsBound(r.start) and r.start <= len then
    pos := r.start;
  else
    pos := 1;
  fi;
  if len = 0 then
    width := 0;
  else
    width := Maximum(List(r.lines, NCurses.WidthAttributeLine));
  fi;
  # skip is the offset for each line it we need to scroll to the right
  skip := 0;
  NCurses.savetty();
  NCurses.SetTerm();
  nk := NCurses.keys;
  ic := INT_CHAR;
  b := NCurses.attrs.BOLD;
  helppage := [
    [b, "'q', <Esc>:"],
    "      quit this pager",
    [b, "<Down>, ' ', 'n', 'd':"],
    "      scroll down one line",
    [b, "<PageDown>, 'N', 'D':"],
    "      scroll down half a page",
    [b, "<Up>, 'p', 'u':"],
    "      scroll up one line",
    [b, "<PageUp>, 'P', 'U':"],
    "      scroll up half a page",
    [b, "<Home>, 'T':"],
    "      goto first (top) line",
    [b, "<End>, 'B', 'G':"],
    "      goto last (bottom) line",
    [b, "<Right>, 'r':"],
    "      scroll right one column",
    [b, "<Left>, 'l':"],
    "      scroll left one column",
    [b, "'0':"],
    "      scroll left to first column",
    [b, "'$':"],
    "      scroll to right most column",
    [b, "'?', <F1>, 'h':"],
    "      show this help"
    ];
  while true do
    # we show lines pos..max
    if len - pos + 1 + off + 1 <= size[1] then
      max := len;
    else
      # reserve one line for " . . ." 
      max := pos + size[1] - off - 3;
    fi;
    NCurses.werase(win);
    for i in [pos..max] do
      if pos > 1 and i = pos then
        NCurses.PutLine(win, off, off, ["   ", sout, "< < <"]);
      else
        NCurses.PutLine(win, off + i - pos, off, r.lines[i], skip);
      fi;
    od;
    if max < len then
      NCurses.PutLine(win, size[1] - 2, off, ["   ", sout, "> > >"]);
    fi;
    NCurses.WBorder(win, r.border);
    NCurses.PutLine(win, size[1] - 1, Maximum(0, QuoInt( size[2] - 
                         NCurses.WidthAttributeLine(r.hint), 2 )), r.hint);
    NCurses.update_panels();
    NCurses.curs_set(0);
    NCurses.doupdate();
    # navigation
    c := NCurses.wgetch(win);
    if c in [ic('q'), 27] then
      break;
    elif c in [nk.UP, ic('u'), ic('p')] then
      if pos > 1 then
        pos := pos - 1;
      fi;
    elif c in [nk.DOWN, ic('d'), ic('n'), ic(' ')] then
      if max < len then
        pos := pos + 1;
      fi;
    elif c in [nk.PPAGE, ic('U'), ic('P')] then
      if pos > 1 then
        pos := pos - QuoInt( size[1]-2, 2 );
        if pos < 1 then
          pos := 1;
        fi;
      fi;
    elif c in [nk.NPAGE, ic('D'), ic('N')] then
      if max < len then
        pos := pos + QuoInt( size[1]-2, 2 );
        if len - pos + 1 + off + 1 <= size[1] then
          pos := len - size[1] + off + 2;
        fi;
      fi;
    elif c in [nk.LEFT, ic('l')] then
      if skip > 0 then
        skip := skip - 1;
      fi;
    elif c in [nk.RIGHT, ic('r')] then
      if skip + size[2] - 2 * off < width then
        skip := skip + 1;
      fi;
    elif c in [ic('0')] then
      skip := 0;
    elif c in [ic('$')] then
      if width > size[2] - 2 * off then
        skip := width + 2 * off - size[2];
      fi;
    elif c in [nk.HOME, ic('T')] then
      pos := 1;
    elif c in [nk.END, ic('B'), ic('G')] then
      pos := len - size[1] + off + 2;
    elif c in [ic('h'), ic('?'), nk.F1] and not IsBound(r.thisishelp) then 
      NCurses.Pager(rec( lines := helppage,
          size := [Minimum(NCurses.getmaxyx(0)[1]-2, Length(helppage)+2),
                  Maximum(List(helppage, Length)) + 2],
          begin := [1, 2],
          border := true,
          hint := " [ q to leave help ] ",
          thisishelp := true ));
    fi;
  od;
  NCurses.ResetCursor();
  NCurses.del_panel(pan);
  NCurses.delwin(win);
  NCurses.resetty();
  NCurses.endwin();
end;

                 
##  <#GAPDoc Label="NCurses.Select">
##  <ManSection>
##  <Func Name="NCurses.Select" Arg="poss[, single[, none]]"/>
##  <Returns>Position or list of positions, or <K>false</K>.</Returns>
##  <Description>
##  This function allows the user to select one or several items from a
##  given list. In the simplest case <Arg>poss</Arg> is a list of attribute 
##  lines (see <Ref Func="NCurses.IsAttributeLine"/>),
##  each of which should fit on one line. Then <Ref Func="NCurses.Select" /> 
##  displays these lines and lets the user browse through them. After pressing
##  the <B>Return</B> key the index of the highlighted item is returned. 
##  Note that attributes in your lines should be switched on and off separately
##  by <K>true</K>/<K>false</K> entries such that the lines can be nicely
##  highlighted.
##  <P/>
##  The optional argument <Arg>single</Arg> must be <K>true</K> (default)
##  or <K>false</K>. In the second case, an arbitrary number of items can be
##  marked and the function returns the list of their indices.
##  <P/>
##  If <Arg>single</Arg> is <K>true</K> a third argument <Arg>none</Arg> can
##  be given. If it is <K>true</K> then it is possible to leave the selection
##  without choosing an item, in this case <K>false</K> is returned.
##  <P/>
##  More details can be given to the function by giving a record as argument
##  <Arg>poss</Arg>. It can have the following components:
##  <List >
##  <Mark><C>items</C></Mark>
##  <Item>The list of attribute lines as described above.</Item>
##  <Mark><C>single</C></Mark>
##  <Item>Boolean with the same meaning as the optional argument 
##  <Arg>single</Arg>.</Item>
##  <Mark><C>none</C></Mark>
##  <Item>Boolean with the same meaning as the optional argument 
##  <Arg>none</Arg>.</Item>
##  <Mark><C>size</C></Mark>
##  <Item>The size of the window like the first two arguments of 
##  <C>NCurses.newwin</C> (default is <C>[0, 0]</C>, as big as possible).
##  </Item>
##  <Mark><C>begin</C></Mark>
##  <Item>Top-left corner of the window like the last two arguments of
##  <C>NCurses.newwin</C> (default is <C>[0, 0]</C>, top-left of the screen).
##  </Item>
##  <Mark><C>attribute</C></Mark>
##  <Item>An attribute used for the display of the window (default is
##  <C>NCurses.attrs.NORMAL</C>).</Item>
##  <Mark><C>border</C></Mark>
##  <Item>Set to <K>true</K> if the window should be displayed with a border
##  (default is <K>false</K>).</Item>
##  <Mark><C>header</C></Mark>
##  <Item>An attribute line used as header line (the default depends on 
##  the settings of <C>single</C> and <C>none</C>).</Item>
##  <Mark><C>hint</C></Mark>
##  <Item>An attribute line used as hint in the last line of the window (the 
##  default depends on the settings of <C>single</C> and <C>none</C>).</Item>
##  </List>
##  <P/>
##  <Example><![CDATA[
##  gap> index := NCurses.Select(["Apples", "Pears", "Oranges"]);
##  gap> index := NCurses.Select(rec(
##  >                     items := ["Apples", "Pears", "Oranges"],
##  >                     single := false,
##  >                     border := true,
##  >                     begin := [5, 5],
##  >                     size := [8, 60],
##  >                     header := "Choose fruits",
##  >                     attribute := NCurses.ColorAttr("yellow","red") ) );
##  ]]></Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
##  r: record with entries
##       .items          list of single line strings to choose from
##       .single         if 'true' (default) one (or none) item is to 
##                       be selected
##       .none           if 'true' a 'q' is allowed to quit without selection
##       .size
##       .begin
##       .attribute
##       .border
##       .header
##       .hint
#args: r[, single[, none]]
NCurses.Select := function(arg)
  local r, t, labels, len, mwin, mpan, size, offitems, start, 
        b, max, ind, sel, pos, draw, c, a, helppage;
  r := arg[1];
  if IsList(r) then
    r := rec(items := r);
  else
    r:= ShallowCopy( r );
  fi;
  if Length(arg) > 1 then
    r.single := arg[2];
  fi;
  if not IsBound(r.single) then
    r.single := true;
  fi;
  if Length(arg) > 2 then
    r.none := arg[3];
  fi;
  if not IsBound(r.none) then
    r.none := false;
  fi;
  if not IsBound(r.size) then
    r.size := [0, 0];
  fi;
  if not IsBound(r.begin) then
    r.begin := [0, 0];
  fi;
  if not IsBound(r.border) then
    r.border := false;
  fi;
  if IsBound( r.select ) then
    r.select:= Intersection( r.select, [ 1 .. Length( r.items ) ] );
    if r.single then
      if r.select = [] then
        r.select:= [ 1 ];
      else
        r.select:= [ r.select[1] ];
      fi;
    fi;
  elif r.single then
    r.select:= [ 1 ];
  else
    r.select:= [];
  fi;

  # default header and hint
  if not IsBound(r.header) then
    if r.single and r.none then
      t := "Choose one or none item:";
    elif r.single then
      t := "Choose one of these items:";
    else
      t := "Select a subset of these items:";
    fi;
    r.header := [NCurses.attrs.BOLD, t];
  fi;
  if not IsBound(r.hint) then
    if r.single and r.none then
      t := " [ <Up>/<Down> select, <Return> done, q none ] ";
    elif r.single then
      t := " [ <Up>/<Down> select, <Return> done ] ";
    else
      t := " [ <Up>/<Down> move, <Space> (un)select, <Return> done ] ";
    fi;
    r.hint := [NCurses.attrs.BOLD, t];
  fi;
  
  # precompute labels
  if r.single then
    labels := List([1..Length(r.items)], i-> Concatenation("[",
                    String(i), "]"));
    len := Length(labels[Length(labels)]) + 2;
    for a in labels do
      while Length(a) < len do
        Add(a, ' ');
      od;
    od;
  fi;
  
  # set standard terminal flags and create window and panel
  NCurses.savetty();
  NCurses.SetTerm();
  mwin := NCurses.newwin(r.size[1], r.size[2], r.begin[1], r.begin[2]);
  if mwin = false then
    mwin := NCurses.newwin(r.size[1], r.size[2], 0, 0);
  fi;
  if mwin = false then
    mwin := NCurses.newwin(0, 0, 0, 0);
  fi;
  if mwin = false then
    return fail;
  fi;
  mpan := NCurses.new_panel(mwin);
  if IsBound(r.attribute) then
    NCurses.wbkgdset(mwin, r.attribute);
  fi;
  size := NCurses.getmaxyx(mwin);
#T why not r.size?
  # offset for viewable items
  offitems := 0;
  # line of first displayed item
  start := 0;
  # number of items that can be displayed
  max := size[1];
  if Length(r.header) > 0 then
    max := max - 1;
    start := start + 1;
  fi;
  if Length(r.hint) > 0 then
    max := max - 1;
  fi;
  if r.border then
    if Length(r.hint) > 0 then
      max := max - 1;
    else
      max := max - 2;
    fi;
    start := start + 1;
  fi;
  # indent of text
  if r.border then
    ind := 1;
  else
    ind := 0;
  fi;
  if not r.single then
    sel := BlistList([1..Length(r.items)], r.select );
  fi;
  # currently active row
  if IsEmpty( r.select ) then
    pos := 1;
  else
    pos:= r.select[1];
  fi;
  # help texts
  b := NCurses.attrs.BOLD;
  if r.single and r.none then
    helppage := [
        [b, "'q', <Esc>:"],
        "      quit without selecting an item" ];
  else
    helppage := [];
  fi;
  if r.single then
    Append(helppage, [
    [b, "<Return>:"],
    "      quit with choosing focussed item",
    ] );
  else
    Append(helppage, [
    [b, "<Return>:"],
    "      finish selection",
    [b, "<Space>, 'x':"],
    "      (un)select focussed item" ]);
  fi;
  Append(helppage, [
    [b, "<Down>, 'n', 'd':"],
    "      focus next item",
    [b, "<PageDown>, 'N', 'D':"],
    "      move focus down half a page",
    [b, "<Up>, 'p', 'u':"],
    "      focus previous item",
    [b, "<PageUp>, 'P', 'U':"],
    "      move focus up half a page",
    [b, "<Home>, 'T':"],
    "      goto first item",
    [b, "<End>, 'B', 'G':"],
    "      goto last item",
    [b, "'h':"],
    "      show this help"
    ] );
  # the loop to (re)draw the window
  draw := function()
    local ii, l, i;
    NCurses.werase(mwin);
    if Length(r.header) > 0 then
      NCurses.PutLine(mwin, start-1, ind, r.header);
    fi;
    for i in [1..max] do
      ii := offitems + i;
      if ii <= Length(r.items) then
        l := [];
        if ii = pos then
          Add(l, NCurses.attrs.STANDOUT);
        else
          Add(l, NCurses.attrs.NORMAL);
        fi;
        if not r.single then
          if sel[ii] then
            Add(l, "[X]  ");
          else
            Add(l, "[ ]  ");
          fi;
        else
          Add(l, labels[ii]);
        fi;
        if IsString(r.items[ii]) then
          Add(l, r.items[ii]);
        else
          Append(l, r.items[ii]);
        fi;
        NCurses.PutLine(mwin, start + i - 1, ind, l);
      fi;
    od;
    if offitems > 0 then
      NCurses.wmove(mwin, start, 0);
      NCurses.wclrtoeol(mwin);
      NCurses.PutLine(mwin, start, ind, " < < <");
    fi;
    if offitems + max < Length(r.items) then
      NCurses.wmove(mwin, start + max - 1, 0);
      NCurses.wclrtoeol(mwin);
      NCurses.PutLine(mwin, start + max - 1, ind, " > > >");
    fi;
    if r.border <> false then
      NCurses.WBorder(mwin, r.border);
    fi;
    if Length(r.hint) > 0 then
      NCurses.PutLine(mwin, size[1] - 1, Maximum(ind + 1, QuoInt(size[2] - 
                      NCurses.WidthAttributeLine(r.hint), 2)), r.hint);
    fi;
  end;
  while true do
    draw();
    NCurses.curs_set(0);
    NCurses.update_panels();
    NCurses.doupdate();
    c := NCurses.wgetch(mwin);
    if c in [INT_CHAR('q'), 27] and r.single and r.none then
      r.RESULT := false;
      break;
    elif c in [NCurses.keys.DOWN, INT_CHAR('n'), INT_CHAR('d')] then
      if pos < Length(r.items) then
        pos := pos + 1;
      fi;
    elif c in [NCurses.keys.UP, INT_CHAR('p'), INT_CHAR('u')]  then
      if pos > 1 then
        pos := pos - 1;
      fi;
    elif c in [NCurses.keys.NPAGE, INT_CHAR('N'), INT_CHAR('D')] then
      pos := pos + QuoInt( max+1, 2 );
      if pos > Length(r.items) then
        pos := Length(r.items);
      fi;
    elif c in [NCurses.keys.PPAGE, INT_CHAR('P'), INT_CHAR('U')] then
      pos := pos - QuoInt( max+1, 2 );
      if  pos < 1 then
        pos := 1;
      fi;
    elif c in [INT_CHAR(' '), INT_CHAR('x')] and not r.single then
      sel[pos] := not sel[pos];
    elif c in [NCurses.keys.HOME, INT_CHAR('T')] then
      pos := 1;
    elif c in [NCurses.keys.END, INT_CHAR('B'), INT_CHAR('G')] then
      pos := Length(r.items);
    elif c in [NCurses.keys.ENTER, 13] then
      if r.single then 
        r.RESULT := pos;
      else
        r.RESULT := Filtered([1..Length(sel)], i-> sel[i]);
      fi;
      break;
    elif c in [INT_CHAR('h')] and not IsBound(r.thisishelp) then 
      NCurses.Pager(rec( lines := helppage,
          size := [Minimum(NCurses.getmaxyx(0)[1]-2, Length(helppage)+2),
                  Maximum(List(helppage, Length)) + 2],
          begin := [1, 2],
          border := true,
          hint := " [ q to leave help ] ",
          thisishelp := true ));
    fi;
    # correct offitems if pos not in visible part
    if pos = 1 then
      offitems := 0;
    elif pos <= offitems + 1 then
      offitems := pos - 2;
    elif pos = Length(r.items) then
      offitems := Maximum(0, Length(r.items) - max);
    elif pos > offitems + max - 1 then
      offitems := pos - max + 1;
    fi;
  od;
  NCurses.del_panel(mpan);
  NCurses.delwin(mwin);
  NCurses.resetty();
  NCurses.endwin();
  return r.RESULT;
end;

#T NCurses.Select( rec( items:= [ "1", "2", "3" ], size:= [ 4, 70 ] ) );
#T does not work correctly: one cannot see the entry "2"


#############################################################################
##
#F  NCurses.Alert( <messages>, <timeout>[, <attrs>] )
##
##  <#GAPDoc Label="Alert_man">
##  <ManSection>
##  <Func Name="NCurses.Alert" Arg="messages, timeout[, attrs]"/>
##
##  <Returns>
##  the integer corresponding to the character entered, or <K>fail</K>.
##  </Returns>
##
##  <Description>
##  In visual mode, <C>Print</C> cannot be used for messages.
##  An alternative is given by <C>NCurses.Alert</C>.
##  <P/>
##  Let <A>messages</A> be either an attribute line or a nonempty list of
##  attribute lines,
##  and <A>timeout</A> be a nonnegative integer.
##  <C>NCurses.Alert</C> shows <A>messages</A> in a bordered box in the
##  middle of the screen.
##  <P/>
##  If <A>timeout</A> is zero then the box is closed after any user input,
##  and the integer corresponding to the entered key is returned.
##  If <A>timeout</A> is a positive number <M>n</M>, say,
##  then the box is closed after <M>n</M> milliseconds,
##  and <C>fail</C> is returned.
##  <P/>
##  If the optional argument <A>attrs</A> is given, it must be an integer
##  representing attributes such as the components of <C>NCurses.attrs</C>
##  (see Section&nbsp;<Ref Subsect="ssec:ncursesAttrs"/>)
##  or the return value of <Ref Func="NCurses.ColorAttr"/>;
##  these attributes are used for the border of the box.
##  The default is <C>NCurses.attrs.NORMAL</C>.
##  <P/>
##  <Example><![CDATA[
##  gap> NCurses.Alert( "Hello world!", 1000 );
##  fail
##  gap> NCurses.Alert( [ "Hello world!",                       
##  >      [ "Hello ", NCurses.attrs.BOLD, "bold!" ] ], 1500,
##  >      NCurses.ColorAttr( "red", -1 ) + NCurses.attrs.BOLD );
##  fail
##  ]]></Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
NCurses.Alert := function( arg )
    local usage, messages, timeout, attrs, yx, height, width, win, pan, i,
          result;

    # Get and check the arguments.
    usage:= "usage: NCurses.Alert( <messages>, <timeout>[, <attrs>] )";
    if Length( arg ) < 2 or 3 < Length( arg ) then
      Error( usage );
    fi;
    messages:= arg[1];
    if NCurses.IsAttributeLine( messages ) then
      messages:= [ messages ];
    elif not ( IsList( messages ) and ForAll( messages,
                                              NCurses.IsAttributeLine )
                                  and not IsEmpty( messages ) ) then
      Error( usage );
    fi;
    timeout:= arg[2];
    if not ( IsInt( timeout ) and 0 <= timeout ) then
      Error( usage );
    fi;
    attrs:= NCurses.attrs.NORMAL;
    if Length( arg ) = 3 then
      attrs:= arg[3];
      if not IsInt( attrs ) then
        Error( usage );
      fi;
    fi;

    # Create a window of the right size in the middle of the screen,
    # and fill it with `messages'.
    yx:= NCurses.getmaxyx( 0 );
    height:= Length( messages ) + 2;
    if yx[1] < height then
      height:= yx[1];
    fi;
    width:= Maximum( List( messages, NCurses.WidthAttributeLine ) ) + 2;
    if yx[2] < width then
      width:= yx[2];
    fi;
    win:= NCurses.newwin( height, width, QuoInt( yx[1] - height, 2 ),
                          QuoInt( yx[2] - width, 2 ) );
    pan:= NCurses.new_panel( win );
    NCurses.savetty();
    NCurses.SetTerm();
    NCurses.curs_set( 0 );
    NCurses.werase( win );
    for i in [ 1 .. Length( messages ) ] do
      NCurses.PutLine( win, i, 1, messages[i] );
    od;
    NCurses.wattrset( win, attrs );
    NCurses.wborder( win, 0 );
    NCurses.wattrset( win, NCurses.attrs.NORMAL );

    # Show the window.
    NCurses.update_panels();
    NCurses.doupdate();

    # Evaluate the criterion for closing the box.
    if timeout = 0 then
      result:= NCurses.wgetch( win );
    else
      NCurses.napms( timeout );
      result:= fail;
    fi;

    # Close the box.
    NCurses.del_panel( pan );
    NCurses.delwin( win );
    NCurses.resetty();
    NCurses.endwin();

    # Return the result.
    return result;
end;


#############################################################################
##
#F  NCurses.NumberOfKey()
##
NCurses.NumberOfKey := function()
    return NCurses.Alert( "Please hit a key", 0 );
end;

## useful for small tests, shows win 0 for 2 seconds and then erases everything
NCurses.SC0 := function()
  NCurses.wrefresh(0);
  Sleep(2);
  NCurses.werase(0);
  NCurses.wrefresh(0);
  Sleep(1);
  NCurses.endwin();
end;


fi;

