GAP 4.8.9 installation with standard packages -- copy to your CoCalc project to get it
#############################################################################
##
#W ncurses.gi GAP 4 package `Browse' Frank Lübeck
##
#Y Copyright (C) 2006-2007, Lehrstuhl D für Mathematik, RWTH Aachen, Germany
##
## 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 <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 <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 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 <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 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 <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 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;