#############################################################################
##
#W  ncurses.gi            GAP 4 package `Browse'                 Frank Lübeck
##
##  This file implements some lower/middle level functionality of the package:
##  GAP level additions to the record NCurses, utilities for attribute lines
##  and basic applications NCurses.{Pager,Select,Alert}.
##  

##  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.IsBackspace( <c> )
##
##  <c> must be an integer.
##  On some systems, the code 127 corresponds to the backspace key
##  but 'NCurses.keys.BACKSPACE' does not.
##
NCurses.IsBackspace:= c -> c in [NCurses.keys.BACKSPACE, IntChar(''), 127];


#############################################################################
##
#F  NCurses.GetCharacterWithReplay( <win>, <replay>, <log> )
##
##  This function does not deal with mouse events!
##
NCurses.GetCharacterWithReplay:= function( win, replay, log )
    local c, currlog;

    if replay = fail or BrowseData.IsDoneReplay( replay ) then
      c:= NCurses.wgetch( win );
    else
      currlog:= replay.logs[ replay.pointer ];
      c:= currlog.steps[ currlog.position ];
      if IsChar( c ) then
        c:= IntChar( c );
      fi;
      currlog.position:= currlog.position + 1;
      if not currlog.quiet then
        NCurses.napms( currlog.replayInterval );
      fi;
    fi;
    if log <> fail and ( replay = fail or 1 < replay.pointer ) then
      Add( log, c );
    fi;

    return c;
end;


#############################################################################
##
#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 <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, len;

    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
          len:= Length( result );
          if IsString( result[ len ] ) then
            result[ len ]:= Concatenation( result[ len ], line );
          else
            Add( result, line );
          fi;
        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;

# args: line[, attr]   # default attr is NCurses.attrs.STANDOUT
# puts attr at beginning and after a 0 entry without following boolean
NCurses.StandOutAttributeLine := function(arg)
  local line, attr, res, len, i;
  line := arg[1];
  if Length(arg) > 1 then
    attr := arg[2];
  else
    attr := NCurses.attrs.STANDOUT;
  fi;
  if IsString(line) then
    return [attr, line];
  fi;
  res := [attr];
  len := Length(line);
  for i in [1..len] do
    if line[i] = 0 and i < len and not IsBool(line[i+1]) then
      Append(res, [0, attr]);
    else
      Add(res, line[i]);
    fi;
  od;
  return res;
end;
  
# we change OnBreak to first leave visual mode
OnBreakSavedByBrowse := OnBreak;
OnBreak := function()
# if NCurses.IsStdoutATty() then
  if not NCurses.isendwin() then
    NCurses.endwin();
  fi;
# fi;
  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] := IntChar(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;

######################################
# Using the mouse:

# names of mouse events in same order as in kernel (but recall that
# positions in kernel are counted from 0)
NCurses.mouseEvents := [ "BUTTON1_PRESSED", "BUTTON1_RELEASED",
"BUTTON1_CLICKED", "BUTTON1_DOUBLE_CLICKED", "BUTTON1_TRIPLE_CLICKED",
"BUTTON2_PRESSED", "BUTTON2_RELEASED", "BUTTON2_CLICKED",
"BUTTON2_DOUBLE_CLICKED", "BUTTON2_TRIPLE_CLICKED", "BUTTON3_PRESSED",
"BUTTON3_RELEASED", "BUTTON3_CLICKED", "BUTTON3_DOUBLE_CLICKED",
"BUTTON3_TRIPLE_CLICKED", "BUTTON4_PRESSED", "BUTTON4_RELEASED",
"BUTTON4_CLICKED", "BUTTON4_DOUBLE_CLICKED", "BUTTON4_TRIPLE_CLICKED",
"BUTTON_SHIFT", "BUTTON_CTRL", "BUTTON_ALT", "REPORT_MOUSE_POSITION",
"BUTTON5_PRESSED", "BUTTON5_RELEASED", "BUTTON5_CLICKED",
"BUTTON5_DOUBLE_CLICKED", "BUTTON5_TRIPLE_CLICKED" ];

##  <#GAPDoc Label="NCurses.Mouse">
##  <ManSection>
##  <Heading>Mouse support in <C>ncurses</C> applications</Heading>
##  <Func Name="NCurses.UseMouse" Arg="on"/>
##  <Returns>a record</Returns>
##  <Func Name="NCurses.GetMouseEvent" Arg=""/>
##  <Returns>a list of records</Returns>
##  <Description>
##  <C>ncurses</C> allows  on some  terminals (<C>xterm</C> and  related) to
##  catch mouse events.  In principle a subset of events  can be caught, see
##  <C>mousemask</C>  in <Ref  Subsect="ssec:ncursesMouse"/>. But  this does
##  not seem to  work well with proper subsets of  possible events (probably
##  due to  intermediate processes X, window  manager, terminal application,
##  ...). Therefore  we suggest to  catch either all  or no mouse  events in
##  applications. <P/>
##  
##  This  can  be done  with  <Ref  Func="NCurses.UseMouse"/> with  argument
##  <K>true</K>  to   switch  on  the   recognition  of  mouse   events  and
##  <K>false</K>  to switch  it  off.  The function  returns  a record  with
##  components  <C>.new</C>  and  <C>.old</C>  which are  both  set  to  the
##  status  <K>true</K> or  <K>false</K>  from after  and  before the  call,
##  respectively.  (There does  not  seem to  be a  possibility  to get  the
##  current  status  without  calling  <Ref  Func="NCurses.UseMouse"/>.)  If
##  you  call the  function with  argument <K>true</K>  and the  <C>.new</C>
##  component  of the  result is  <K>false</K>, then  the terminal  does not
##  support mouse events.<P/>
##  
##  When the  recognition of mouse events  is switched on and  a mouse event
##  occurs  then the  key <C>NCurses.keys.MOUSE</C>  is found  in the  input
##  queue, see <C>wgetch</C> in  <Ref Subsect="ssec:ncursesInput"/>. If this
##  key  is read  the  low level  function  <C>NCurses.getmouse</C> must  be
##  called to  fetch further details about  the event from the  input queue,
##  see <Ref Subsect="ssec:ncursesMouse"/>.  In many cases this  can be done
##  by calling  the function <Ref Func="NCurses.GetMouseEvent"/>  which also
##  generates  additional  information.  The  return  value  is  a  list  of
##  records,  one  for  each  panel  over which  the  event  occured,  these
##  panels  sorted from  top to  bottom (so,  often you  will just  need the
##  first  entry if  there is  any). Each  of these  records has  components
##  <C>.win</C>,  the  corresponding  window  of the  panel,  <C>.y</C>  and
##  <C>.x</C>,  the relative  coordinates  in window  <C>.win</C> where  the
##  event occured, and  <C>.event</C>, which is bound to one  of the strings
##  in <C>NCurses.mouseEvents</C> which describes the event. <P/>
##  
##  <Emph>Suggestion:</Emph> Always  make the use  of the mouse  optional in
##  your application. Allow the user to  switch mouse usage on and off while
##  your  application is  running. Some  users may  not like  to give  mouse
##  control  to your  application, for  example the  standard cut  and paste
##  functionality cannot be used while mouse events are caught.
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##  
NCurses.UseMouse := function(on)
  local res, a;
  if on = true then
    res := NCurses.mousemask([0..29]+0);
  else
    res := NCurses.mousemask([]);
  fi;
  for a in ["new", "old"] do
    if Length(res.(a)) > 0 then
      res.(a) := true;
    else
      res.(a) := false;
    fi;
  od;
  return res;
end;

# Add triples [win, yrel, xrel] to result of NCurses.getmouse for each
# window win below the position [y, x], the windows in top panels coming
# first; yrel, xrel are the coordinates of y, x relative to window win.
NCurses.AddMouseWins := function(mev)
  local res, p, beg;
  res := [];
  p := NCurses.panel_below(0);
  while p <> false do
    if NCurses.wenclose(p, mev[1], mev[2]) then
      beg := NCurses.getbegyx(p);
      Add(res, [p, mev[1] - beg[1], mev[2] - beg[2]]);
    fi;
    p := NCurses.panel_below(p);
  od;
  mev[4] := res;
  return mev;
end;

NCurses.GetMouseEvent := function()
  local raw, res, a;
  # get y,x,eventlist wrt. screen
  raw := NCurses.getmouse();
  # compute the  panel windows under y, x and relative positions
  NCurses.AddMouseWins(raw);
  # make list of records with .win, .y, .x (relative to .win), .event (as name)
  # (in most applications only  the first for the top panel under the 
  # event is needed)
  res := [];
  for a in raw[4] do
    Add(res, rec(win := a[1], y := a[2], x := a[3], 
                 event := NCurses.mouseEvents[raw[3][1]+1]));
  od;
  return res;
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>
##
##  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, IntChar);
  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"/>
##  <Returns>an attribute for setting the foreground and background color 
##  to be used on a terminal window (it is a &GAP; integer).</Returns>
##  <Var Name="NCurses.attrs.has_colors" />
##  <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> if NCurses.IsStdoutATty() then
##  >      NCurses.update_panels();; NCurses.doupdate();;
##  >      NCurses.napms(5000);;     # show for 5 seconds
##  >      NCurses.endwin();; NCurses.del_panel(pan);; NCurses.delwin(win);;
##  >    fi;
##  ]]></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 then
    bg := 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. 
##  <Log><![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();
##  ]]></Log>
##  </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/>
##  <Log><![CDATA[
##  gap> str := NCurses.GetLineFromUser("Your Name: ");;
##  gap> Print("Hello ", str, "!\n");
##  ]]></Log>
##  </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, IntChar('')] then
      pos := 1;
    elif c in [NCurses.keys.END, IntChar('')] then
      pos := Length(res) + 1;
      if pos > max then
        pos := pos - 1;
      fi;
    elif NCurses.IsBackspace( c ) then
      if pos > 1 then
        pos := pos - 1;
        RemoveElmList(res, pos);
      fi;
    elif c in [NCurses.keys.DC, IntChar('')] then
      if pos <= Length(res) then
        RemoveElmList(res, pos);
      fi;
    elif not c in [ NCurses.keys.ENTER, IntChar(NCurses.CTRL('M')), 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, IntChar(NCurses.CTRL('M')), 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;


#############################################################################
##
#F  NCurses.EditFields( <win>, <arecs> )
##
##  For a list <arecs> of records, allows the user to edit some fields
##  in the window <win>.
##  <TAB> sets the focus to the next field,
##  <RETURN> stops the cycle and transfers the changed values,
##  <ESC> stops the cycle without transfering the changed values.
##
##  If <arecs> is a record then it must have the component 'fields'.
##  Optional components are 'replay' and 'log', which are used in some
##  applications based on 'NCurses.BrowseGeneric'.
##
NCurses.EditFields := function( win, arecs )
    local replay, log, results, i, yx, curs, field, createfield, fillfield,
          b, helppage, arec, res, max, pos, firstvisible, ins, c;

    # Initializations.
    replay:= fail;
    log:= fail;
    if IsRecord( arecs ) then
      if IsBound( arecs.replay ) then
        replay:= arecs.replay;
      fi;
      if IsBound( arecs.log ) then
        log:= arecs.log;
      fi;
      arecs:= arecs.fields;
    fi;
    results:= List( arecs, x -> "" );
    for i in [ 1 .. Length( arecs ) ] do
      if not IsRecord( arecs[i] ) then
        arecs[i]:= rec( prefix:= arecs[i], suffix:= "" );
      fi;
      if IsBound( arecs[i].default ) then
        results[i]:= arecs[i].default;
      fi;
    od;
    yx:= NCurses.getmaxyx( win );

    NCurses.savetty();
    NCurses.SetTerm();

    curs:= NCurses.curs_set( 1 );  # make sure cursor is visible

    # Determine the focus.
    field:= PositionProperty( arecs,
                r -> IsBound( r.focus ) and r.focus = true );
    if field = fail then
      field:= 1;
      arecs[1].focus:= true;
    fi;

    # Make all editable fields visible.
    createfield:= function( arec )
      if not IsBound( arec.begin ) then
        arec.begin:= [ yx[1]-4, 2 ];
      fi;
      if not IsBound( arec.nrows ) then
        arec.nrows:= 1;
      fi;
      if not IsBound( arec.ncols ) then
        arec.ncols:= yx[2]-6;
      elif arec.ncols = "fit" then
        arec.ncols:= Length( arec.prefix ) + Length( arec.default )
                                           + Length( arec.suffix );
      fi;

      # Delete the window if it exists already, and create it anew.
      # (Without this, setting the cursor does not work correctly.)
      if IsBound( arec.win ) then
        NCurses.del_panel( arec.pan );
        NCurses.delwin( arec.win );
      fi;
      arec.win:= NCurses.newwin( arec.nrows + 2, arec.ncols + 2,
                                 arec.begin[1], arec.begin[2] );
      arec.pan:= NCurses.new_panel( arec.win );
    end;

    fillfield:= function( arec, res, pos, firstvisible, max, hasfocus )
      local line, showres;

      NCurses.werase( arec.win );

      # Highlight the border of the field if it has the focus.
      if hasfocus then
        NCurses.wattrset( arec.win, NCurses.attrs.BOLD );
        NCurses.wborder( arec.win, 0 );
        NCurses.wattrset( arec.win, NCurses.attrs.NORMAL );
      else
        NCurses.wborder( arec.win, 0 );
      fi;

      # Cut out invisible parts of the string,
      # and enter the current contents.
      showres:= res;
      line:= [ NCurses.attrs.BOLD, arec.prefix, NCurses.attrs.NORMAL ];
      if 1 < firstvisible then
        showres:= res{ [ firstvisible .. Length( res ) ] };
      fi;
      if Length( showres ) <= max then
        Append( line, [ showres, NCurses.attrs.BOLD, arec.suffix ] );
      else
        Append( line, [ showres{ [ 1 .. max ] },
                        NCurses.attrs.BOLD, arec.suffix ] );
      fi;
      NCurses.PutLine( arec.win, 1, 1, line );

      # Show the continuation symbols if applicable.
      if 1 < firstvisible then
        NCurses.wmove( arec.win, 1, Length( arec.prefix ) + 1 );
        NCurses.waddch( arec.win, NCurses.lineDraw.CKBOARD );
      fi;
      if max < Length( showres ) then
        NCurses.wmove( arec.win, 1, arec.ncols );
        NCurses.waddch( arec.win, NCurses.lineDraw.CKBOARD );
      fi;
      NCurses.wmove( arec.win, 1,
                     Length( arec.prefix ) + pos - firstvisible + 1 );
    end;

    for i in [ 1 .. Length( arecs ) ] do
      if i <> field then
        arec:= arecs[i];
        createfield( arec );
        max:= arec.ncols - Length( arec.prefix ) - Length( arec.suffix );
        fillfield( arec, results[i], 1, 1, max, false );
      fi;
    od;
    NCurses.curs_set( 1 );
    arec:= arecs[ field ];
    createfield( arec );
    max:= arec.ncols - Length( arec.prefix ) - Length( arec.suffix );
    fillfield( arec, results[ field ], 1, 1, max, true );

    # Prepare a help menu.
    b:= NCurses.attrs.BOLD;
    helppage:= [
      [b, "<Esc>:"],
      "      quit without submitting values",
      [b, "<Return>:"],
      "      submit the current values",
    ];
    if 1 < Length( arecs ) then
      Append(helppage, [
        [b, "<Tab>:"],
        "      move focus to the next field",
        ] );
    fi;
    Append(helppage, [
      [b, "<Left>:"],
      "      move cursor left",
      [b, "<Right>:"],
      "      move cursor right",
      [b, "<Home>:"],
      "      move cursor to the beginning",
      [b, "<End>:"],
      "      move cursor to the end",
      [b, "<Del>:"],
      "      delete character under cursor",
      [b, "<Backspace>:"],
      "      delete character left from cursor",
      [b, "<Insert>:"],
      "      toggle insertion/overwrite mode",
      [b, "<F1>:"],
      "      show this help",
      ] );

    # Start the loop over the fields.
    while true do

      # Edit this field.
      arec:= arecs[ field ];
      res:= results[ field ];
      max:= arec.ncols - Length( arec.prefix ) - Length( arec.suffix );
      pos:= 1;
      firstvisible:= 1;
      ins:= true;
      createfield( arec );

      while true do
        fillfield( arec, res, pos, firstvisible, max, true );
        NCurses.update_panels();
        NCurses.doupdate();

        # Get a character and adjust the data.
        c:= NCurses.GetCharacterWithReplay( arec.win, replay, log );
        if c = NCurses.keys.RIGHT then
          if pos <= Length( res ) then
            pos:= pos + 1;
            if max < pos - firstvisible + 1 or
               ( max = pos - firstvisible + 1 and pos < Length( res ) ) then
              firstvisible:= firstvisible + 1;
            fi;
          fi;
        elif c = NCurses.keys.LEFT then
          if 1 < pos then
            pos:= pos - 1;
            if pos = firstvisible and 1 < firstvisible then
              firstvisible:= firstvisible - 1;
            fi;
          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, IntChar('') ] then
          pos:= 1;
          firstvisible:= 1;
        elif c in [ NCurses.keys.END, IntChar('') ] then
          pos:= Length( res ) + 1;
          firstvisible:= pos - max + 1;
          if firstvisible < 1 then
            firstvisible:= 1;
          fi;
        elif NCurses.IsBackspace( c ) then
          if pos > 1 then
            pos:= pos - 1;
            RemoveElmList( res, pos );
            if pos = firstvisible and 1 < firstvisible then
              firstvisible:= firstvisible - 1;
            fi;
          fi;
        elif c in [ NCurses.keys.DC, IntChar('') ] then
          if pos <= Length( res ) then
            RemoveElmList( res, pos );
          fi;
        elif c in [ NCurses.keys.ENTER, IntChar(NCurses.CTRL('M')), 27, 9 ] then
          break;
        elif c in [ NCurses.keys.F1 ] 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 ));
        else
          if ins then
            InsertElmList( res, pos, CHAR_INT( c mod 256 ) );
            pos:= pos + 1;
            if max < pos - firstvisible + 1 or
               ( max = pos - firstvisible + 1 and pos < Length( res ) ) then
              firstvisible:= firstvisible + 1;
            fi;
          else
            res[pos]:= CHAR_INT( c mod 256 );
            pos:= pos + 1;
            if max < pos - firstvisible + 1 or
               ( max = pos - firstvisible + 1 and pos < Length( res ) ) then
              firstvisible:= firstvisible + 1;
            fi;
          fi;
        fi;
      od;

      results[ field ]:= res;
      if c = 9 then
        # TAB was entered; the focus leaves this window,
        # the window border is no longer highlighted.
        NCurses.wattrset( arec.win, NCurses.attrs.NORMAL );
        NCurses.wborder( arec.win, 0 );

        field:= field + 1;
        if field > Length( arecs ) then
          field:= 1;
        fi;
      else
        break;
      fi;
    od;

    # Clean up.
    for arec in arecs do
      if IsBound( arec.win ) then
        NCurses.del_panel( arec.pan);
        NCurses.delwin( arec.win);
      fi;
    od;
    NCurses.curs_set( curs );
    NCurses.resetty();
    NCurses.endwin();

    # <ESC> was pressed.
    if c = 27 then
      results:= fail;
    fi;

## If this was called from visual mode, one should now say:  
##    NCurses.update_panels();
##    NCurses.doupdate();

    return results;
end;


#############################################################################
##
#F  NCurses.EditFieldsDefault( <title>, <labels>, <defaults>, <winwidth>,
#F                             <replay>, <log> )
##
##  This function is a wrapper for 'NCurses.EditFields'.
##  It returns the result of this function.
##
##  It is crucial that 'NCurses.update_panels()' and 'NCurses.doupdate()'
##  are not called at the end of this function,
##  because otherwise the error messages from a subsequent validation
##  (as in 'BrowseUserPreferences') would become visible
##  in visual mode.
##
NCurses.EditFieldsDefault:= function( title, labels, defaults, winwidth,
                                      replay, log )
    local fields, n, j, header, hint, ncols, win, pan, result;

    fields:= [];
    n:= 3;
    for j in [ 1 .. Length( labels ) ] do
      fields[j]:= rec( prefix:= labels[j],
                       suffix:= "",
                       default:= String( defaults[j], 0 ),
                       begin:= [ n + 2, 4 ],
                       ncols:= winwidth - 10,
                     );
      n:= n + 3;
    od;

    header:= [ NCurses.attrs.BOLD, title, NCurses.attrs.NORMAL ];
    if Length( defaults ) = 1 then
      hint:= [ NCurses.attrs.BOLD,
               " [ <Return> done, <Esc> cancel, <F1> help ] ",
               NCurses.attrs.NORMAL ];
    else
      hint:= [ NCurses.attrs.BOLD,
        " [ <Tab> move focus, <Return> done, <Esc> cancel, <F1> help ] ",
               NCurses.attrs.NORMAL ];
    fi;
    ncols:= winwidth - 6;
    win:= NCurses.newwin( n+2, ncols, 2, 3 );
    pan:= NCurses.new_panel( win );
    NCurses.wattrset( win, NCurses.attrs.BOLD );
    NCurses.wborder( win, 0 );
    NCurses.wattrset( win, NCurses.attrs.NORMAL );
    NCurses.PutLine( win, 1, 2, header );
    NCurses.PutLine( win, n+1,
      Int( ( ncols - NCurses.WidthAttributeLine( hint ) ) / 2 ), hint );
    result:= NCurses.EditFields( win, rec( fields:= fields,
                                           replay:= replay,
                                           log:= log ) );
    NCurses.del_panel( pan );
    NCurses.delwin( win );

## If this was called from visual mode, one should now say:  
##    NCurses.update_panels();
##    NCurses.doupdate();

    return result;
end;


#############################################################################
##
#F  NCurses.EditTable( <arec> )
##
##  <arec> must be a record with the following components.
##  - 'list'
##      the list of records to be edited,
##  - 'title'
##      a string, used as the title of the dialog windows,
##  - 'choices'
##      optional, a list of records from which one can choose,
##  - 'rectodisp'
##      a function that takes a record from 'list' and returns a string
##      that is shown in the table, in order to show the current choices,
##  - 'mapping'
##      a list of pairs `[ component, label ]' such that 'component' is the
##      name of a component in the entries in 'list', and 'label' is the
##      label shown in the edit box for the record.
##
##  Optional components are 'replay' and 'log', which are used inside
##  'NCurses.Select' and 'NCurses.EditFields'.
##
##  The function admits a rudimentary editing of 'list', that is, one can
##  - delete entries,
##  - edit those components of entries that occur in 'mapping',
##  - add new entries to 'list', either by choosing them from 'choices'
##    or by entering an entry by hand; in the latter case, this affects only
##    the components that occur in 'mapping'.
##
##  Note:
##  - 'list' and its entries are changed in place.
##  - It is not possible to reorder the entries in 'list'.
##  - Only strings are supported as values of the record components;
##    more precisely, any component value will be converted to a string on
##    editing the record.
##
##  The function returns 'true' if something was edited,
##  and 'false' if not.
##
NCurses.EditTable:= function( arec )
    local replay, log, choices, index, available, new, entry, r, defaults, i;

    replay:= fail;
    log:= fail;
    if IsBound( arec.replay ) then
      replay:= arec.replay;
    fi;
    if IsBound( arec.log ) then
      log:= arec.log;
    fi;

    if 0 < Length( arec.list ) then
      # Let the user choose an action.
      choices:= rec(
        header:= "Please choose an action.",
        items:= [ "Add new entries", "Edit an entry", "Delete entries" ],
        single:= true,
        none:= true,
        border:= NCurses.attrs.BOLD,
        align:= "c",
        size:= "fit",
        replay:= replay,
        log:= log,
      );
      index:= NCurses.Select( choices );
      if index = false then
        return false;
      fi;
    else
      # The only possible action is to add an entry.
      index:= 1;
    fi;

    if   index = 1 then
      # Add new entries
      if IsBound( arec.choices ) then
        # Let the user choose one or more entries from this list.
        available:= Filtered( arec.choices, r -> not r in arec.list );
        choices:= Concatenation( [ "create new entry" ],
                                 List( available, arec.rectodisp ) );
        choices:= rec(
          header:= "Please choose entries to be added.",
          items:= choices,
          single:= false,
          none:= true,
          border:= NCurses.attrs.BOLD,
          align:= "c",
          size:= "fit",
          replay:= replay,
          log:= log,
        );
        index:= NCurses.Select( choices );
        if index = [] then
          return false;
        fi;

        # Extend the list.
        if 1 in index then
          new:= true;
          index:= index{ [ 2 .. Length( index ) ] } - 1;
        else
          new:= false;
          index:= index - 1;
        fi;
        Append( arec.list, available{ index } );
      else
        # Create a new entry.
        new:= true;
      fi;

      # Let the user create a new entry if wanted.
      if new then
        entry:= NCurses.EditFieldsDefault( arec.title,
                  List( arec.mapping, x -> Concatenation( x[2], ": " ) ),
                  List( arec.mapping, x -> "" ),
                  NCurses.getmaxyx( 0 )[2],
                  replay,
                  log );
        NCurses.update_panels();
        NCurses.doupdate();
        if entry <> fail then
          r:= rec();
          for i in [ 1 .. Length( arec.mapping ) ] do
            r.( arec.mapping[i][1] ):= entry[i];
          od;
          Add( arec.list, r );
        fi;
      fi;

    elif index = 2 then
      # Edit an entry:
      if 1 < Length( arec.list ) then
        # Let the user choose one entry from the current list.
        choices:= rec(
          header:= "Please choose an entry to be edited.",
          items:= List( arec.list, arec.rectodisp ),
          single:= true,
          none:= true,
          border:= NCurses.attrs.BOLD,
          align:= "c",
          size:= "fit",
          replay:= replay,
          log:= log,
        );
        index:= NCurses.Select( choices );
        if index = false then
          return false;
        fi;
      else
        # There is no choice.
        index:= 1;
      fi;

      # Let the user edit this entry.
      r:= arec.list[ index ];
      defaults:= [];
      for i in [ 1 .. Length( arec.mapping ) ] do
        if IsBound( r.( arec.mapping[i][1] ) ) then
          defaults[i]:= r.( arec.mapping[i][1] );
        else
          defaults[i]:= "";
        fi;
      od;
      entry:= NCurses.EditFieldsDefault( arec.title,
                List( arec.mapping, x -> Concatenation( x[2], ": " ) ),
                defaults,
                NCurses.getmaxyx( 0 )[2],
                replay,
                log );
      NCurses.update_panels();
      NCurses.doupdate();
      if entry <> fail then
        # The user did not cancel.
        for i in [ 1 .. Length( arec.mapping ) ] do
          r.( arec.mapping[i][1] ):= entry[i];
        od;
      fi;

    else
      # Delete entries:
      # Let the user choose one or more entries from the current list.
      choices:= rec(
        header:= "Please choose entries to be deleted.",
        items:= List( arec.list, arec.rectodisp ),
        single:= false,
        none:= true,
        border:= NCurses.attrs.BOLD,
        align:= "c",
        size:= "fit",
        replay:= replay,
        log:= log,
      );
      index:= NCurses.Select( choices );
      if index = false then
        return false;
      fi;

      # Delete the entries in question.
      arec.list:= arec.list{ Difference( [ 1 .. Length( arec.list ) ],
                                         index ) };
    fi;

    # Probably there was a change.
    return true;
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/>
##  <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.
##  
##  <Log><![CDATA[
##  gap> lines := List([1..100],i-> ["line ",NCurses.attrs.BOLD,String(i)]);;
##  gap> NCurses.Pager(lines);
##  ]]></Log>
##  </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;
  
  # If we know that there will be no chance to show anything
  # in visual mode then print a warning and give up.
  if not IsBound( GAPInfo.SystemEnvironment.TERM )
     or GAPInfo.SystemEnvironment.TERM = "dumb" then
    Info( InfoWarning, 1,
    "NCurses.Pager: cannot switch to visual mode because of TERM = \"dumb\"" );
    return fail;
  fi;

  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 := IntChar;
  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>
##  <Index Subkey="see NCurses.Select">checkbox</Index>
##  <Index Subkey="see NCurses.Select">radio button</Index>
##  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),
##  or the string <C>"fit"</C> which means the smallest possible window.
##  </Item>
##  <Mark><C>align</C></Mark>
##  <Item>
##    A substring of <C>"bclt"</C>, which describes the alignment of the
##    window in the terminal.
##    The meaning and the default are the same as for
##    <Ref Func="BrowseData.IsBrowseTableCellData"/>.
##  </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).
##    This value has priority over the <C>align</C> component.
##  </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>
##    If the window should be displayed with a border then set to
##    <K>true</K> (default is <K>false</K>) or to 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>.
##  </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>
##  <Mark><C>onSubmitFunction</C></Mark>
##  <Item>
##    A function that is called when the user submits the selection;
##    the argument for this call is the current value of the record
##    <A>poss</A>.
##    If the function returns <K>true</K> then the selected entries are
##    returned as usual,
##    otherwise the selection window is kept open, waiting for new inputs;
##    if the function returns a nonempty list of attribute lines then
##    these messages are shown using <Ref Func="NCurses.Alert"/>.
##  </Item>
##  </List>
##  <P/>
##  If mouse events are enabled
##  (see <Ref Func="NCurses.UseMouse"/>)<Index>mouse events</Index>
##  then the window can be moved on the screen via mouse events,
##  the focus can be moved to an entry,
##  and (if <C>single</C> is <K>false</K>) the selection of an entry can be
##  toggled.
##  <P/>
##  <Log><![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 at least two fruits",
##  >                     attribute := NCurses.ColorAttr("yellow","red"),
##  >                     onSubmitFunction:= function( r )
##  >                       if Length( r.RESULT ) < 2 then
##  >                         return [ "Choose at least two fruits" ];
##  >                       else
##  >                         return true;
##  >                       fi;
##  >                     end ) );
##  ]]></Log>
##  </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           window size or "fit"
##       .align          a substring of "bclt"
##       .begin
##       .attribute
##       .border         'false' (default) or 'true' or an integer denoting
##                       attributes of the border
##       .header
##       .hint
##       .onSubmitFunction
##
NCurses.Select_Match:= function( entry, searchstr, case_sensitive, mode,
                                 type, negate )
    local pos, len, pos2;

    # Remove markup if necessary.
    entry:= NCurses.SimpleString( entry );
    if not case_sensitive then
      entry:= LowercaseString( entry );
      searchstr:= LowercaseString( searchstr );
    fi;

    if mode = "substring" then
      pos:= PositionSublist( entry, searchstr );
      if pos <> fail and type = "any substring" then
        return not negate;
      fi;
      len:= Length( entry );
      while pos <> fail do
        if type = "word" then
          pos2:= pos + Length( searchstr ) - 1;
          if ( pos = 1 or entry[ pos-1 ] = ' ' ) and
             ( pos2 = len or
               ( pos2 < len and entry[ pos2+1 ] = ' ' ) ) then
            return not negate;
          fi;
        elif type = "prefix" then
          if pos = 1 or ( pos < len and entry[ pos-1 ] = ' ' ) then
            return not negate;
          fi;
        elif type = "suffix" then
          pos2:= pos + Length( searchstr ) - 1;
          if pos2 = len or
             ( pos2 < len and entry[ pos2 + 1 ] = ' ' ) then
            return not negate;
          fi;
        else
          Error( "not supported as <type>: ", type );
        fi;
        pos:= PositionSublist( entry, searchstr, pos );
      od;
      return negate;
    else
      if   type = "\"=\"" then
        return ( entry = searchstr ) <> negate;
      elif type = "\"<>\"" then
        return ( entry = searchstr ) = negate;
      else
        if   type = "\"<\"" then
          return ( entry < searchstr ) <> negate;
        elif type = "\"<=\"" then
          return ( entry <= searchstr ) <> negate;
        elif type = "\">=\"" then
          return ( entry >= searchstr ) <> negate;
        else # type = "\">\""
          return ( entry > searchstr ) <> negate;
        fi;
      fi;
    fi;
end;

NCurses.Select_SearchPattern:= function( items, string, currpos, incl,
                                         parameters, isvisible )
    local direction, entry, wrap, case, mode, type, negate, n, range2,
          range1, i;

    # Evaluate the search parameters:
    # - forwards (default) or backwards?
    # - wrap around (default) or not?
    # - case sensitive (default) or not?
    # - search for (1) any substring or (2) for whole entries?
    #   - in case (1), search for substring, word, prefix, suffix?
    #   - in case (2), compare via <, <=, =, >=, >, <>?
    # - negated search or not (default)?
    direction:= "forwards";
    for entry in parameters do
      if entry[1] = "search" and entry[2][1] = "forwards" then
        direction:= entry[2][ entry[3] ];
      elif entry[1] = "wrap around" then
        wrap:= ( entry[2][ entry[3] ] = "yes" );
      elif entry[1] = "case sensitive" then
        case:= ( entry[2][ entry[3] ] = "yes" );
      elif entry[1] = "mode" then
        mode:= entry[2][ entry[3] ];
      elif entry[1] = "search for" and mode = "substring" then
        type:= entry[2][ entry[3] ];
      elif entry[1] = "compare with" and mode = "whole entry" then
        type:= entry[2][ entry[3] ];
      elif entry[1] = "negate" then
        negate:= ( entry[2][ entry[3] ] = "yes" );
      fi;
    od;

    n:= Length( items );
    range2:= [];
    if direction = "forwards" then
      if not incl then
        currpos:= currpos + 1;
        if currpos = n + 1 then
          if wrap then
            currpos:= 1;
          else
            return fail;
          fi;
        fi;
      fi;
      range1:= [ currpos .. n ];
      if wrap then
        range2:= [ 1 .. currpos - 1 ];
      fi;
    else
      if not incl then
        currpos:= currpos - 1;
        if currpos = 0 then
          if wrap then
            currpos:= n;
          else
            return fail;
          fi;
        fi;
      fi;
      range1:= [ currpos, currpos - 1 .. 1 ];
      if wrap then
        range2:= [ n, n - 1 .. currpos + 1 ];
      fi;
    fi;

    for i in range1 do
      if isvisible[i] and
         NCurses.Select_Match( items[i], string, case, mode, type, negate )
        then
        return i;
      fi;
    od;
    for i in range2 do
      if isvisible[i] and
         NCurses.Select_Match( items[i], string, case, mode, type, negate )
        then
        return i;
      fi;
    od;

    return fail;
end;

NCurses.Select_FilterList:= function( items, string, parameters, isvisible )
    local entry, case, mode, type, negate, nrvisible, newisvisible, i;

    # Evaluate the filtering parameters:
    # - case sensitive (default) or not?
    # - search for (1) any substring or (2) for whole entries?
    #   - in case (1), search for substring, word, prefix, suffix?
    #   - in case (2), compare via <, <=, =, >=, >, <>?
    # - negated search or not (default)?
    for entry in parameters do
      if   entry[1] = "case sensitive" then
        case:= ( entry[2][ entry[3] ] = "yes" );
      elif entry[1] = "mode" then
        mode:= entry[2][ entry[3] ];
      elif entry[1] = "search for" and mode = "substring" then
        type:= entry[2][ entry[3] ];
      elif entry[1] = "compare with" and mode = "whole entry" then
        type:= entry[2][ entry[3] ];
      elif entry[1] = "negate" then
        negate:= ( entry[2][ entry[3] ] = "yes" );
      fi;
    od;

    nrvisible:= 0;
    newisvisible:= ShallowCopy( isvisible );
    for i in [ 1 .. Length( items ) ] do
      if isvisible[i] then
        if NCurses.Select_Match( items[i], string, case, mode, type, negate )
          then
          nrvisible:= nrvisible + 1;
        else
          newisvisible[i]:= false;
        fi;
      fi;
    od;

    return [ newisvisible, nrvisible ];
end;

#args: r[, single[, none]]
NCurses.Select := function(arg)
  local r, t, labels, len, mwin, winparas, mpan, size, offitems, offitemsv,
        start, b, max, ind, sel, pos, draw, c, a, helppage,
        searchString, searchParameters, filterParameters, isvisible,
        nrvisible, jumpto, cand, digits, replay, log, currlog, buttondown,
        resetjump, str, pos2, move, onsubmit, candlen, i,
        data, event, pressdata, diff, newpos;

  # If we know that there will be no chance to show anything
  # in visual mode then print a warning and give up.
  if not IsBound( GAPInfo.SystemEnvironment.TERM )
     or GAPInfo.SystemEnvironment.TERM = "dumb" then
    Info( InfoWarning, 1,
    "NCurses.Select: cannot switch to visual mode because of TERM = \"dumb\"" );
    return fail;
  fi;

  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.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;
  else
    len:= 5;
  fi;

  # window size and alignment
  size:= NCurses.getmaxyx(0);
  if not IsBound(r.size) then
    # maximal possible window size, alignment questions do not arise
    r.size := size;
    if not IsBound(r.begin) then
      r.begin := [0, 0];
    fi;
  else
    if r.size = "fit" then
      r.size:= [ Length( r.items ) + 3,
                 Maximum( NCurses.WidthAttributeLine( r.header ) + 4,
                          NCurses.WidthAttributeLine( r.hint ) + 6,
                          Maximum( List( r.items, Length ) ) + len + 4 ) ];
    fi;

    # The window size cannot be larger than the terminal size.
    # (Scrolling would not work if 'r.size' is too large.)
    if size[1] < r.size[1] then
      r.size:= [ size[1], r.size[2] ];
    fi;
    if size[2] < r.size[2] then
      r.size:= [ r.size[1], size[2] ];
    fi;

    # alignment of the window in the terminal
    if not IsBound(r.begin) then
      r.begin := [0, 0];
      if IsBound( r.align ) then
        if 'c' in r.align then
          # horizontally centered
          r.begin[2]:= QuoInt( size[2] - r.size[2] + 1, 2 );
        elif not 'l' in r.align then
          # default: right aligned
          r.begin[2]:= size[2] - r.size[2];
        fi;
        if 'b' in r.align then
          # bottom aligned
          r.begin[1]:= size[1] - r.size[1];
        elif not 't' in r.align then
          # default: vertically centered
          r.begin[1]:= QuoInt( size[1] - r.size[1] + 1, 2 );
        fi;
      fi;
    fi;

  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]);
  winparas:= [ 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);
    winparas:= [ r.size[1], r.size[2], 0, 0 ];
  fi;
  if mwin = false then
    mwin := NCurses.newwin(0, 0, 0, 0);
    winparas:= [ 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;
  # offset for viewable items
  offitems := 0;
  offitemsv:= 0;
  # line of first displayed item
  start := 0;
  # number of items that can be displayed
  max:= r.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 <> false 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 <> false 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', 'Q', <Esc>:"],
        "      quit without selecting an item" ];
  else
    helppage := [];
  fi;
  if r.single then
    Append(helppage, [
    [b, "<Return>:"],
    "      choose focussed item",
    ] );
  else
    Append(helppage, [
    [b, "<Return>:"],
    "      finish selection",
    [b, "<Space>, 'x':"],
    "      (un)select focussed item",
    ] );
  fi;
  Append(helppage, [
    [b, "<Down>, '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, "<nr>:"],
    "      goto the item with label <nr>",
    [b, "'/':"],
    "      ask for a search string, and search",
    [b, "'n':"],
    "      search further with the same search string",
    [b, "'f':"],
    "      ask for a filtering string, and filter",
    [b, "'!':"],
    "      reset the filtering",
    [b, "'M':"],
    "      toggle enabling/disabling mouse events",
    [b, "<Mouse1Down>:"],
    "      starting point for moving the window",
    [b, "<Mouse1Up>:"],
    "      end point for moving the window",
    [b, "<Mouse1Click>:"],
    "      move the focus or toggle the selection",
    [b, "<Mouse1DoubleClick>:"],
    "      move the focus and toggle the selection",
    [b, "<F1>, 'h':"],
    "      show this help",
    ] );

  searchString:= "";
  searchParameters:= StructuralCopy( Filtered(
                         BrowseData.defaults.dynamic.searchParameters,
                         l -> not ( "row by row" in l[2] ) ) );
  filterParameters:= StructuralCopy(
                         BrowseData.defaults.dynamic.filterParameters );
  isvisible:= BlistList( [ 1 .. Length( r.items ) ],
                         [ 1 .. Length( r.items ) ] );
  nrvisible:= Length( r.items );
  jumpto:= "[";
  cand:= "";
  digits:= List( "0123456789", IntChar );

  if IsBound( r.replay ) then
    replay:= r.replay;
  else
    replay:= fail;
  fi;
  if IsBound( r.log ) then
    log:= r.log;
  else
    log:= fail;
  fi;

  # the loop to (re)draw the window
  draw := function()
    local ipos, ii, l, i;
    NCurses.werase(mwin);
    if Length(r.header) > 0 then
      NCurses.PutLine(mwin, start-1, ind, r.header);
    fi;
    ipos:= 0;
    for ii in [ offitems + 1 .. Length( r.items ) ] do
      if isvisible[ ii ] then
        ipos:= ipos + 1;
        if max < ipos then
          break;
        fi;
        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
          if ii = pos then
            Append(l, NCurses.StandOutAttributeLine(r.items[ii]));
          else
            Append(l, r.items[ii]);
          fi;
        fi;
        NCurses.PutLine(mwin, start + ipos - 1, ind, l);
      fi;
    od;
    if offitemsv > 0 then
      NCurses.wmove(mwin, start, 0);
      NCurses.wclrtoeol(mwin);
      NCurses.PutLine(mwin, start, ind, " < < <");
    fi;
    if offitemsv + max < nrvisible then
      NCurses.wmove(mwin, start + max - 1, 0);
      NCurses.wclrtoeol(mwin);
      NCurses.PutLine(mwin, start + max - 1, ind, " > > >");
    fi;
    if r.border = true then
      NCurses.wborder( mwin, 0 );
    elif IsInt( r.border ) then
      NCurses.wattrset( mwin, r.border );
      NCurses.wborder( mwin, 0 );
      NCurses.wattrset( mwin, NCurses.attrs.NORMAL );
    elif r.border <> false then
      NCurses.WBorder(mwin, r.border);
    fi;
    if Length(r.hint) > 0 then
      NCurses.PutLine(mwin, winparas[1] - 1, Maximum(ind + 1,
                      QuoInt(winparas[2] - 
                      NCurses.WidthAttributeLine(r.hint), 2)), r.hint);
    fi;
  end;

  # move the window via mouse actions
  buttondown:= false;

  while true do
    draw();
    NCurses.curs_set(0);
    NCurses.update_panels();
    NCurses.doupdate();
    resetjump:= true;
    c:= NCurses.GetCharacterWithReplay( mwin, replay, log );
    if c in [ IntChar( 'q' ), IntChar( 'Q' ), 27 ]
       and r.single and r.none then
      r.RESULT := false;
      break;
    elif c in [NCurses.keys.DOWN, IntChar('d')] then
      # Move to next visible entry if there is one.
      pos2:= pos + 1;
      while pos2 <= Length( r.items ) and not isvisible[ pos2 ] do
        pos2:= pos2 + 1;
      od;
      if pos2 <= Length( r.items ) then
        pos:= pos2;
      fi;
    elif c in [NCurses.keys.UP, IntChar('p'), IntChar('u')]  then
      # Move to the previous visible entry if there is one.
      pos2:= pos - 1;
      while 0 < pos2 and not isvisible[ pos2 ] do
        pos2:= pos2 - 1;
      od;
      if 0 < pos2 then
        pos:= pos2;
      fi;
    elif c in [NCurses.keys.NPAGE, IntChar('N'), IntChar('D')] then
      # Move half a screen down if possible.
      move:= QuoInt( max+1, 2 );
      pos2:= pos + 1;
      while 0 < move and pos2 <= Length( r.items ) do
        if isvisible[ pos2 ] then
          pos:= pos2;
          move:= move - 1;
        fi;
        pos2:= pos2 + 1;
      od;
      if pos > Length(r.items) then
        pos := Length(r.items);
      fi;
    elif c in [NCurses.keys.PPAGE, IntChar('P'), IntChar('U')] then
      # Move half a screen up if possible.
      move:= QuoInt( max+1, 2 );
      pos2:= pos - 1;
      while 0 < move and 0 < pos2 do
        if isvisible[ pos2 ] then
          pos:= pos2;
          move:= move - 1;
        fi;
        pos2:= pos2 - 1;
      od;
      if  pos < 1 then
        pos := 1;
      fi;
    elif c in [IntChar(' '), IntChar('x')] and not r.single then
      # Toggle the selection at `pos'.
      sel[pos] := not sel[pos];
    elif c in [NCurses.keys.HOME, IntChar('T')] then
      # Move to the first visible entry.
      pos2:= 1;
      while pos2 < pos and not isvisible[ pos2 ] do
        pos2:= pos2 + 1;
      od;
      pos:= pos2;
    elif c in [NCurses.keys.END, IntChar('B'), IntChar('G')] then
      # Move to the last visible entry.
      pos2:= Length( r.items );
      while pos < pos2 and not isvisible[ pos2 ] do
        pos2:= pos2 + 1;
      od;
      pos:= pos2;
    elif c in [NCurses.keys.ENTER, 13] then
      # Submit.
      if r.single then 
        r.RESULT := pos;
      else
        r.RESULT := Filtered([1..Length(sel)], i-> sel[i]);
      fi;
      if IsBound( r.onSubmitFunction ) then
        # Leave the visual mode.
        NCurses.endwin();

        # Call the function.
        onsubmit:= r.onSubmitFunction( r );

        if onsubmit = true then
          # Exit the loop.
          break;
        fi;

        # Re-enter the visual mode.
        NCurses.update_panels();
        NCurses.doupdate();
        NCurses.curs_set( 0 );

        # If the return value is a nonempty string then show it.
        if IsList( onsubmit ) and not IsEmpty( onsubmit )
           and ForAll( onsubmit, NCurses.IsAttributeLine ) then
          NCurses.Alert( onsubmit, 0 );
        fi;
      else
        break;
      fi;
    elif c in [ IntChar( '/' ) ] then 
      str:= BrowseData.GetPatternEditParameters(
                [ NCurses.attrs.BOLD, "enter a search string: " ],
                searchString,
                searchParameters );
      if str <> fail and str <> "" then
        searchString:= str;
        pos2:= NCurses.Select_SearchPattern( r.items, searchString, pos,
                   true, searchParameters, isvisible );
        if pos2 = fail then
          NCurses.Alert( [ "not found:", searchString ], 0,
                         NCurses.attrs.BOLD );
        else
          pos:= pos2;
        fi;
      fi;
    elif c in [ IntChar( 'n' ) ] then 
      if searchString <> "" then
        pos2:= NCurses.Select_SearchPattern( r.items, searchString, pos,
                   false, searchParameters, isvisible );
        if pos2 = fail then
          NCurses.Alert( [ "not found:", searchString ], 0,
                         NCurses.attrs.BOLD );
        else
          pos:= pos2;
        fi;
      fi;
    elif c in [ IntChar( 'f' ) ] then
      str:= BrowseData.GetPatternEditParameters(
                [ NCurses.attrs.BOLD, "enter a search string: " ],
                searchString,
                filterParameters );
      if str <> fail and str <> "" then
        searchString:= str;
        str:= NCurses.Select_FilterList( r.items, searchString,
                                         searchParameters, isvisible );
        if str[2] = 0 then
          NCurses.Alert( [ "not found:", searchString ], 0,
                         NCurses.attrs.BOLD );
        else
          isvisible:= str[1];
          nrvisible:= str[2];
          if not isvisible[ pos ] then
            for pos2 in [ pos + 1 .. Length( r.items ) ] do
              if isvisible[ pos2 ] then
                pos:= pos2;
                break;
              fi;
            od;
          fi;
          if not isvisible[ pos ] then
            for pos2 in [ 1 .. pos ] do
              if isvisible[ pos2 ] then
                pos:= pos2;
                break;
              fi;
            od;
          fi;
        fi;
      fi;
    elif c in [ IntChar( '!' ) ] then
      isvisible:= BlistList( [ 1 .. Length( r.items ) ],
                             [ 1 .. Length( r.items ) ] );
    elif c in digits or NCurses.IsBackspace( c ) then
      # Entering or removing numbers is supported only in 'single' mode.
      if r.single then
        if c in digits then
          # If the input corresponds to a label in the list
          # then extend the prefix and jump to the first matching line.
          cand:= Concatenation( jumpto, String( Position( digits, c ) - 1 ) );
          candlen:= Length( cand );
        else
          # If the user had entered as least one digit then delete the last
          # digit from the buffer, and jump to the first matching line.
          candlen:= Length( jumpto );
          if 1 < candlen then
            Unbind( jumpto[ candlen ] );
            candlen:= candlen - 1;
          fi;
        fi;
        for i in [ 1 .. Length( r.items ) ] do
          if isvisible[i]
             and candlen <= Length( labels[i] )
             and labels[i]{ [ 1 .. candlen ] } = cand then
            jumpto:= cand;
            pos:= i;
            break;
          fi;
        od;
        resetjump:= false;
      fi;
    elif c in [ NCurses.keys.F1, IntChar('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 ));
    elif c in [ IntChar( 'M' ) ] then
      # Toggle mouse events.
      data:= NCurses.UseMouse( true );
      if data.old = true or data.new = false then
        data:= NCurses.UseMouse( false );
        NCurses.Alert( [ "mouse events disabled" ], 0, NCurses.attrs.BOLD );
      else
        NCurses.Alert( [ "mouse events enabled" ], 0, NCurses.attrs.BOLD );
      fi;
    elif c = NCurses.keys.MOUSE then
      data:= NCurses.GetMouseEvent();
      if 0 < Length( data ) then
        # There is a indow below the position where the mouse was released.
        # (If not then we cannot move the window.)
        event:= data[1].event;
        if   event = "BUTTON1_PRESSED" and data[1].win = mwin then
          # Store the info where the button was pressed.
          pressdata:= data[ Length( data ) ];
          buttondown:= true;
        elif event = "BUTTON1_RELEASED" and buttondown then
          # Move the window by the difference.
          data:= data[ Length( data ) ];
          winparas[3]:= Maximum( 0, winparas[3] + data.y - pressdata.y );
          winparas[4]:= Maximum( 0, winparas[4] + data.x - pressdata.x );
          NCurses.del_panel( mpan );
          NCurses.delwin( mwin );
          mwin:= CallFuncList( NCurses.newwin, winparas );
          mpan:= NCurses.new_panel( mwin );
          if IsBound( r.attribute ) then
            NCurses.wbkgdset( mwin, r.attribute );
          fi;
          NCurses.update_panels();
          NCurses.doupdate();
          buttondown:= false;
        elif event = "BUTTON1_CLICKED" and data[1].win = mwin then
          # If the button was clicked on an entry then move the focus there.
          # If the focus was already there and if multiple entries can be
          # selected then toggle the selection.
          diff:= data[1].y - start;
          newpos:= diff + offitems + 1;
          if newpos = pos then
            if not r.single then
              # Toggle the selection at `pos'.
              sel[ pos ]:= not sel[ pos ];
            fi;
          elif ( 0 < diff or ( offitemsv = 0 and 0 <= diff ) ) and
               ( diff < max - 1 or ( offitemsv + max >= nrvisible and
                                     diff <= max - 1 ) ) then
            pos:= newpos;
          fi;
        elif event = "BUTTON1_DOUBLE_CLICKED" and data[1].win = mwin then
          # If the button was clicked on an entry then move the focus there,
          # and toggle the selection if multiple entries can be selected.
          diff:= data[1].y - start;
          newpos:= diff + offitems + 1;
          if ( 0 < diff or ( offitemsv = 0 and 0 <= diff ) ) and
             ( diff < max - 1 or ( offitemsv + max >= nrvisible and
                                   diff <= max - 1 ) ) then
            pos:= newpos;
            if not r.single then
              sel[ pos ]:= not sel[ pos ];
            fi;
          fi;
        fi;
      fi;
    fi;
    if resetjump then
      # We have read a non-digit, reset the buffer.
      jumpto:= "[";
    fi;
    # correct offitems, offitemsv if pos not in visible part
    pos2:= Number( [ 1 .. pos ], i -> isvisible[i] );
    if pos2 = 1 then
      offitemsv:= 0;
    elif pos2 <= offitemsv + 1 then
      offitemsv:= pos2 - 2;
    elif pos2 = nrvisible then
      offitemsv:= Maximum(0, nrvisible - max);
    elif pos2 > offitemsv + max - 1 then
      offitemsv:= pos2 - max + 1;
    fi;
    offitems:= PositionNthTrueBlist( isvisible, offitemsv + 1 ) - 1;
  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, <Ref Func="Print" BookName="ref"/> cannot be used for
##  messages.
##  An alternative is given by <Ref Func="NCurses.Alert"/>.
##  <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.
##  <Ref Func="NCurses.Alert"/> 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 <K>fail</K> is returned.
##  <P/>
##  If <C>timeout</C> is zero and mouse events are enabled 
##  (see <Ref Func="NCurses.UseMouse"/>)<Index>mouse events</Index>
##  then the box can be moved inside the window via mouse events.
##  <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, winposy,
          winposx, buttondown, win, pan, i, result, data, event, pressdata;

    # 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;

    if not IsBound( GAPInfo.SystemEnvironment.TERM )
       or GAPInfo.SystemEnvironment.TERM = "dumb" then
      Info( InfoWarning, 1,
      "NCurses.Alert: cannot switch to visual mode because of TERM = \"dumb\"" );
      return fail;
    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;

    # If output is redirected to a file then exit.
    if not NCurses.IsStdoutATty() then
      return fail;
    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;
    winposy:= QuoInt( yx[1] - height, 2 );
    winposx:= QuoInt( yx[2] - width, 2 );
    buttondown:= false;

    repeat
      win:= NCurses.newwin( height, width, winposy, winposx );
      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 );
        if result = NCurses.keys.MOUSE then
          # If the first button is pressed on this window
          # then we expect that the alert box shall be moved.
          # If the first button is released somewhere
          # then we move the alert box by the difference.
          data:= NCurses.GetMouseEvent();
          if 0 < Length( data ) then
            event:= data[1].event;
            if   event = "BUTTON1_PRESSED" and data[1].win = win then
              pressdata:= data[ Length( data ) ];
              buttondown:= true;
            elif event = "BUTTON1_RELEASED" and buttondown then
              data:= data[ Length( data ) ];
              winposy:= Minimum( Maximum( 0, winposy + data.y - pressdata.y ),
                                 yx[1] - height );
              winposx:= Minimum( Maximum( 0, winposx + data.x - pressdata.x ),
                                 yx[2] - width );
              buttondown:= false;
            fi;
          fi;
        fi;
      else
        NCurses.napms( timeout );
        result:= fail;
      fi;

      # Close the box.
      NCurses.del_panel( pan );
      NCurses.delwin( win );
    until result <> NCurses.keys.MOUSE or
          not event in [ "BUTTON1_PRESSED", "BUTTON1_RELEASED" ];
    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;

