### ----------------------------------------------------
### -- Gzz::Text::Utils
### -- Licenses: LGPL-3.0-or-later
### -- Authors: Francis Grizzly Smit <grizzly@smit.id.au>
### -- File: rakulib/Gzz/Text/Utils.rakumod
### ----------------------------------------------------
### -- Chunk 7 of 8
» = $val;
            return True;
        }
        when 'Num'   {
            my Num:D $val = $edit.Num;
            @candidates[$choice]«value» = $val;
            return True;
        }
        when 'Rat'   {
            my Rat:D $val = $edit.Rat;
            @candidates[$choice]«value» = $val;
            return True;
        }
        when 'Bool'  {
            my Bool:D $val = ! %candidate«value»; # toggle it #
            @candidates[$choice]«value» = $val;
            return True;
        }
        when 'Str'   {
            @candidates[$choice]«value» = $edit;
            return True;
        }
        when 'Array' {
            my @edit = $edit.split(rx/ ',' \s* /);
            @candidates[$choice]«value» = @edit;
            return True;
        }
    }
    return False;
} #`««« sub default-edit(Int:D $choice, @candidates is copy, Str:D $edit --> Bool:D) »»»

=begin pod

=head3 input-menu(…)

=begin code :lang<raku>

sub input-menu(@candidates is copy, Str:D $message = "",
                              :&row:(Int:D $c, Int:D $p, @a,
                                     Bool:D :$colour = False, Bool:D :$syntax = False,
                                     Str:D :$highlight-bg-colour = '', Str:D :$highlight-fg-colour = '',
                                     Str:D :$bg-colour0 = '', Str:D :$fg-colour0 = '', 
                                     Str:D :$bg-colour1 = '', Str:D :$fg-colour1 = '' --> Str:D) = &default-row-input-menu, 
                              :&value:(Int:D $c, @a --> MultiT) = &default-value, 
                              :&elt-prompt:(Int:D $c, @a,
                                     Bool:D :$colour = False, Bool:D :$syntax = False,
                                     Str:D :$bg-prompt = '', Str:D :$fg-prompt = '' --> Str:D)  = &default-prompt,
                              :&edit:(Int:D $c, @a is copy, Str:D $e --> Bool:D) = &default-edit, 
                              Bool:D :c(:color(:$colour)) is copy = False,
                              Bool:D :s(:$syntax) = False, 
                              Str:D :$highlight-bg-colour = t.bg-color(0, 0, 127) ~ t.bold, 
                              Str:D :$highlight-fg-colour = t.bright-yellow, 
                              Str:D :$bg-colour0 = t.bg-yellow ~ t.bold, 
                              Str:D :$fg-colour0 = t.bright-blue, 
                              Str:D :$bg-colour1 = t.bg-color(0, 127, 0) ~ t.bold, 
                              Str:D :$fg-colour1 = t.bright-blue,  
                              Str:D :$bg-prompt = t.bg-green ~ t.bold, 
                              Str:D :$fg-prompt = t.bright-blue, 
                              Bool:D :$wrap-around = False --> MultiT) is export 

=end code

=item1 Where:
=item2 B<C<@candidates>> is an array of hashes to make up the rows of the menu.
=item2 B<C<$message>> is a message to be displayed at the top of the ascii text form of things (i.e. no colourising).
=item2 B<C<&row>> is is a callback to deal with the rows of the menu.
=item3 Where
=item4 B<C<$c>> is the current row count.
=item4 B<C<$p>> is the current position in the @candidates array.
=item4 B<C<@a>> is the array @candidates itself.
=item4 B<C<$highlight-bg-colour>> is the background colour of the current row (i.e. $c == $p).
=item4 B<C<$highlight-fg-colour>> is the foreground colour of the current row (i.e. $c == $p).
=item4 B<C<$bg-colour0>> is the background colour of the row (i.e. $c %% 2).
=item4 B<C<$fg-colour0>> is the foreground colour of the row (i.e. $c %% 2).
=item4 B<C<$bg-colour1>> is the background colour of the row (i.e. $c % 2 != 0 or not $c %% 2).
=item4 B<C<$fg-colour1>> is the foreground colour of the row (i.e. $c % 2 != 0).
=item2 B<C<&value>> is a callback to get the return value for the function.
=item4 B<C<$c>> is the row selected.
=item4 B<C<$c>> is the array @candidates.

=begin item2

B«C«&elt-prompt:(Int:D $c, @a,
                 Bool:D :$colour = False, Bool:D :$syntax = False,
                 Str:D :$bg-prompt = '', Str:D :$fg-prompt = '' --> Str:D)  = &default-prompt»» 

The callback called by the function to get the prompts to let the user edit the value of the row.

=end item2

=item3 B<C<$c>> is the entry choosen by the user.
=item3 B<C<@a>> is the candidates array.
=item3 B<C<:c(:color(:$colour))>> defines a boolean flag to tell whether to use colours or not.
=item4 you can use B<C<:c>>, B<C<:color>> or B<C<:colour>> for this they are all exactly the same.
=item3 B<C<:s(:$syntax)>> same as B<C<$colour>> except it could result in some sort of syntax highlighting. 
=item3 B<C<$bg-prompt>>  the background colour to use on the prompt line below the selection area.
=item3 B<C<$fg-prompt>>  the foreground colour to use on the prompt line below the selection area.

=begin item2

B«C«&edit:(Int:D $c, @a is copy, Str:D $e --> Bool:D)  = &default-edit»»

The callback called by the function to set the new value of the row.

=end item2

=item3 B<C<$c>> is the entry choosen by the user.
=item3 B<C<@a>> is the candidates array.

=item2 B<C<:c(:color(:$colour))>> defines a boolean flag to tell whether to use colours or not.
=item3 you can use B<C<:c>>, B<C<:color>> or B<C<:colour>> for this they are all exactly the same.
=item2 B<C<:s(:$syntax)>> same as B<C<$colour>> except it could result in some sort of syntax highlighting. 
=item2 B<C<$highlight-bg-colour>>  the background colour to use to highlight the current line.
=item2 B<C<$highlight-fg-colour>>  the foreground colour to use to highlight the current line.
=item2 B<C<$bg-colour0>> the background colour to use if the line count is divisible by 2.
=item2 B<C<$fg-colour0>> the foreground colour to use if the line count is divisible by 2.
=item2 B<C<$bg-colour1>> the background colour to use if the line count is not divisible by 2.
=item2 B<C<$fg-colour1>> the foreground colour to use if the line count is not divisible by 2.
=item2 B<C<$bg-prompt>>  the background colour to use on the prompt line below the selection area.
=item2 B<C<$fg-prompt>>  the foreground colour to use on the prompt line below the selection area.
=item2 B<C<$wrap-around>> if true then the selection area wraps around, (i.e going past the end wraps around, instead of refusing to go there).
=item3 B<C<$highlight-bg-colour>> to B<C<$wrap-around>> are all just used for the dropdown case (i.e. B<C<$colour>> or B<C<$syntax>> are True)
=item3 B<C<$syntax>> is no different from B<C<$colour>> unless the user defines it using the B<C<:&row>> parameter.
=item4 calls L<dropdown|#dropdown> to do the colour work.
=item4 B<NB: the colours stuff is not yet implemented>

L<Top of Document|#table-of-contents>

=end pod

sub input-menu(@candidates is copy, Str:D $message = "",
                              :&row:(Int:D $c, Int:D $p, @a,
                                     Bool:D :$colour = False, Bool:D :$syntax = False,
                                     Str:D :$highlight-bg-colour = '', Str:D :$highlight-fg-colour = '',
                                     Str:D :$bg-colour0 = '', Str:D :$fg-colour0 = '', 
                                     Str:D :$bg-colour1 = '', Str:D :$fg-colour1 = '' --> Str:D) = &default-row-input-menu, 
                              :&value:(Int:D $c, @a --> MultiT) = &default-value, 
                              :&elt-prompt:(Int:D $c, @a,
                                     Bool:D :$colour = False, Bool:D :$syntax = False,
                                     Str:D :$bg-prompt = '', Str:D :$fg-prompt = '' --> Str:D)  = &default-prompt,
                              :&edit:(Int:D $c, @a is copy, Str:D $e --> Bool:D) = &default-edit, 
                              Bool:D :c(:color(:$colour)) is copy = False,
                              Bool:D :s(:$syntax) = False, 
                              Str:D :$highlight-bg-colour = t.bg-color(0, 0, 127) ~ t.bold, 
                              Str:D :$highlight-fg-colour = t.bright-yellow, 
                              Str:D :$bg-colour0 = t.bg-yellow ~ t.bold, 
                              Str:D :$fg-colour0 = t.bright-blue, 
                              Str:D :$bg-colour1 = t.bg-color(0, 127, 0) ~ t.bold, 
                              Str:D :$fg-colour1 = t.bright-blue,  
                              Str:D :$bg-prompt = t.bg-green ~ t.bold, 
                              Str:D :$fg-prompt = t.bright-blue, 
                              Bool:D :$wrap-around = False --> MultiT) is export {
    $colour = True if $syntax;
    my %cancel = type => 'read-only', value => 'cancel', name => 'cancel';
    @candidates.push(%cancel);
    my %ok = type => 'read-only', value => 'OK', name => 'OK';
    @candidates.push(%ok);
    if $colour {
        die "colour stuff not implemented yet!!!";
        # insure that the screen is reset on error #
        my &stack = sub ( --> Nil) {
            while @signal {
                my &elt = @signal.pop;
                &elt();
            }
        };
        signal(SIGINT, SIGHUP, SIGQUIT, SIGTERM, SIGQUIT).tap( { &stack(); put t.restore-screen; say "$_ Caught"; exit 0 } );
        my &setup-option-str = sub (Int:D $cnt, Int:D $pos, @array --> Str:D ) {
            return &row($cnt, $pos, @array, :$colour, :$syntax, 
                        :$highlight-bg-colour, :$highlight-fg-colour,
                        :$bg-colour0, :$fg-colour0, 
                        :$bg-colour1, :$bg-colour1);
        };
        my &get-result = sub (MultiT:D $result, Int:D $pos, Int:D $length, @array --> MultiT:D ) {
            my $res = $result;
            if $pos ~~ 0..^$length {
                $res = @array[$pos]«value»;
            }
            return $res
        };
        my &find-pos = sub (MultiT $result, Int:D $pos is copy, @array --> Int:D) {
            for @array.kv -> $idx, %r {
                if %r«value» eq $result {
                    $pos = $idx;
                    last; # found so don't waste resources #
                }
            }
            return $pos;
        }
        my Str:D $result = dropdown(@candidates[@candidates.elems - 1]«value», 40, 'backup',
                                                    &setup-option-str, &find-pos, &get-result,
                                                    @candidates, 
                                                    :$highlight-bg-colour,
                                                    :$highlight-fg-colour,
                                                    :$bg-colour0,
                                                    :$fg-colour0,
                                                    :$bg-colour1,
                                                    :$fg-colour1,
                                                    :$bg-prompt,
                                                    :$fg-prompt,
                                                    :$wrap-around);
        return $result;
    }
    put t.save-screen;
    my $choice = -1;
    my $gzzreadline        = Gzz_readline.new;
    OUTTER: loop {
        put t.clear-screen;
        $message.say if $message;
        for @candidates.kv -> $indx, %row {
            my Str:D $candidate = &elt-prompt($indx, @candidates) ~ &row($indx, $indx, @candidates);
            "%10d\t%-20s\n".printf($indx, $candidate)
        }
        "use cancel, bye, bye bye, quit, q, or {+@candidates - 1} to quit or enter to accept the values as is".say;
        loop {
            $choice = prompt("choose a candiate 0..{+@candidates - 1} =:> ");
            $choice = +@candidates - 1 if $choice eq ''; # i.e. an  simple enter #
            $choice = +@candidates - 2 if $choice ~~ rx:i/ ^^ \s* [ 'cancel' || 'bye' [ \s* 'bye' ] ? || 'quit' || 'q' ] \s* $$ /;
            if $choice !~~ rx/ ^^ \s* \d* \s* $$ / {
                "$choice: is not a valid option".say;
                redo OUTTER;
            }
            unless 0 <= $choice < +@candidates {
                "$choice: is not a valid option".say;
                redo OUTTER;
            }
            if &row($choice, $choice, @candidates) eq 'cancel' {
                last OUTTER;
            } elsif &row($choice, $choice, @candidates) eq 'OK' {
                last OUTTER;
            } else {
                my %row = @candidates[$choice];
                given %row«type».tclc {
                    when 'Int'   {
                        my Str:D $edit = gzzreadline_call(&elt-prompt($choice, @candidates), ~&value($choice, @candidates), $gzzreadline);
                        &edit($choice, @candidates, $edit);
                        next OUTTER;
                    }
                    when 'Num'   {
                        my Str:D $edit = gzzreadline_call(&elt-prompt($choice, @candidates), ~&value($choice, @candidates), $gzzreadline);
                        &edit($choice, @candidates, $edit);
                        next OUTTER;
                    }
                    when 'Rat'   {
                        my Str:D $edit = gzzreadline_call(&elt-prompt($choice, @candidates), ~&value($choice, @candidates), $gzzreadline);
                        &edit($choice, @candidates, $edit);
                        next OUTTER;
                    }
                    when 'Bool'  {
                        my Str:D $edit = &value($choice, @candidates) ?? 'True' !! 'False';
                        &edit($choice, @candidates, $edit);
                        next OUTTER;
                    }
                    when 'Str'   {
                        my Str:D $edit = gzzreadline_call(&elt-prompt($choice, @candidates), ~&value($choice, @candidates), $gzzreadline);
                        &edit($choice, @candidates, $edit);
                        next OUTTER;
                    }
                    when 'Array' {
                        my Str:D $edit = gzzreadline_call(&elt-prompt($choice, @candidates), ~&value($choice, @candidates), $gzzreadline);
                        &edit($choice, @candidates, $edit);
                        next OUTTER;
                    }
                } # given %row«type».tclc #
            } # if &row($choice, $choice, @candidates) eq 'cancel' ... elsif &row($choice, $choice, @candidates) eq 'OK' ... else ... #
            last;
        } # loop #
    } # OUTTER: loop #
    my @result;
    if &row($choice, $choice, @candidates) eq 'OK' {
        for @candidates -> %row {
            next if %row«name» eq 'OK' || (%row«name» eq 'cancel');
            @result.push: %row;
        }
    }
    put t.restore-screen;
    return @result;
} #`««« sub input-menu(@candidates is copy, Str:D $message = "",
                              :&row:(Int:D $c, Int:D $p, @a,
                                     Bool:D :$colour = False, Bool:D :$syntax = False,
                                     Str:D :$highlight-bg-colour = '', Str:D :$highlight-fg-colour = '',
                                     Str:D :$bg-colour0 = '', Str:D :$fg-colour0 = '', 
                                     Str:D :$bg-colour1 = '', Str:D :$fg-colour1 = '' --> Str:D) = &default-row-input-menu, 
                              :&value:(Int:D $c, @a --> MultiT) = &default-value, 
                              :&elt-prompt:(Int:D $c, @a,
                                     Bool:D :$colour = False, Bool:D :$syntax = False,
                                     Str:D :$bg-prompt = '', Str:D :$fg-prompt = '' --> Str:D)  = &default-prompt,
                              :&edit:(Int:D $c, @a is copy, Str:D $e --> Bool:D) = &default-edit, 
                              Bool:D :c(:color(:$colour)) is copy = False,
                              Bool:D :s(:$syntax) = False, 
                              Str:D :$highlight-bg-colour = t.bg-color(0, 0, 127) ~ t.bold, 
                              Str:D :$highlight-fg-colour = t.bright-yellow, 
                              Str:D :$bg-colour0 = t.bg-yellow ~ t.bold, 
                              Str:D :$fg-colour0 = t.bright-blue, 
                              Str:D :$bg-colour1 = t.bg-color(0, 127, 0) ~ t.bold, 
                              Str:D :$fg-colour1 = t.bright-blue,  
                              Str:D :$bg-prompt = t.bg-green ~ t.bold, 
                              Str:D :$fg-prompt = t.bright-blue, 
                              Bool:D :$wrap-around = False --> MultiT) is export »»»

=begin pod


=head3 dropdown(…)

A text based dropdown/list or menu with ANSI colours.

=begin code :lang<raku>

sub dropdown(MultiT:D $id, Int:D $window-height is copy, Str:D $id-name,
                        &setup-option-str:(Int:D $c, Int:D $p, @a --> Str:D),
                        &find-pos:(MultiT $r, Int:D $p, @a --> Int:D),
                        &get-result:(MultiT:D $res, Int:D $p, Int:D $l, @a --> MultiT:D),
                        @array,
                        Str:D :$highlight-bg-colour = t.bg-color(0, 0, 127) ~ t.bold, 
                        Str:D :$highlight-fg-colour = t.bright-yellow, 
                        Str:D :$bg-colour0 = t.bg-yellow ~ t.bold, 
                        Str:D :$fg-colour0 = t.bright-blue, 
                        Str:D :$bg-colour1 = t.bg-color(0, 127, 0) ~ t.bold, 
                        Str:D :$fg-colour1 = t.bright-blue,  
                        Str:D :$bg-prompt = t.bg-green ~ t.bold, 
                        Str:D :$fg-prompt = t.bright-blue, 
                        Bool:D :$wrap-around = False --> MultiT) is export  

=end code

=item Where
=item2 B<C<$id>>               is the starting value of our position in the array/choices.
=item2 B<C<$window-height>>    is the number of rows of characters to display at a time.
=item2 B<C<$id-name>>          is the name of the parameter we are scrolling.
=item2 B<C<&setup-option-str>> is a function that returns the current row.
=item3 Where:
=item4 the arg B<C<$c>> will be the count of the row we are drawing.
=item4 the arg B<C<$p>> will be the position in the array we are at. 
=item4 the arg B<C<@a>> will be the B<C<@array>> supplied to B<C<dropdown(…)>> 
=item5 the use of a function for this means you can compute a much more complex field.
=item2 B<C<&find-pos>> is a function that finds the start position in the B<C<dropdown>>.
=item3 Where:
=item4 the arg B<C<$r>> is the value in the array B<C<@array>> to look for.
=item4 the arg B<C<$p>> is the best approximation of where it might be if you are using it in a loop or something it could be where it last was. 
=item4 the arg B<C<@a>> the argument B<C<@array>> that was passed to B<C<dropdown>>.
=item5 you can name these argument anything you like in you function, and because of the computed nature of this function and the other two you have great flexibility.
=item2 B<C<&get-result>>       is a function to work out the value selected.
=item3 Where:
=item4 the arg B<C<$res>> is the default value to return.
=item4 the arg B<C<$p>> is the current position in the array B<C<@array>> supplied to B<C<dropdown>>.
=item4 the arg B<C<$l>> is the length of the array B<C<@array>>.
=item4 the arg B<C<@a>> is the array B<C<@array>> that was supplied to B<C<dropdown>>.
=item2 B<C<@array>> the array of rows to display.
=item2 B<C<$highlight-bg-colour>>  the background colour to use to highlight the current line.
=item2 B<C<$highlight-fg-colour>>  the foreground colour to use to highlight the current line.
=item2 B<C<$bg-colour0>> the background colour to use if the line count is divisible by 2.
=item2 B<C<$fg-colour0>> the foreground colour to use if the line count is divisible by 2.
=item2 B<C<$bg-colour1>> the background colour to use if the line count is not divisible by 2.
=item2 B<C<$fg-colour1>> the foreground colour to use if the line count is not divisible by 2.
=item2 B<C<$bg-prompt>>  the background colour to use on the prompt line below the selection area.
=item2 B<C<$fg-prompt>>  the foreground colour to use on the prompt line below the selection area.
=item2 B<C<$wrap-around>> if true then the selection area wraps around, (i.e going past the end wraps around, instead of refusing to go there).

=begin item3 

Because we use a function we can compute much more complex results; depending on what we have in B<C<@array>>.
The result can be any of Str, Int, Rat or Num see L<MultiT|#multit>.

=end item3

Here is an example of use.

L<Top of Document|#table-of-contents>

=begin code :lang<raku>

my &setup-option-str = sub (Int:D $cnt, Int:D $p, @array --> Str:D ) {
    my Str $name;
    my Str $cc;
    my Str $flag;
    my Str $prefix;
    if $cnt < 0 {
        $name   = "No country selected yet.";
        $cc     = "";
        $flag   = "";
        $prefix = "you must choose one";
    } else {
        my %row = @array[$cnt];
        $name   = %row«_name»;
        $cc     = %row«cc»;
        try {
            CATCH {
                default {
                    my $Name = $name;
                    $Name ~~ s:g/ <wb> 'and' <wb> /\&/;
                    try {
                        CATCH {
                            default { $flag = uniparse 'PENGUIN'}
                        }
                        $flag = uniparse $Name;
                    }
                }
            }
            $flag   = uniparse $name;
        }
        $prefix = %row«prefix»;
    }
    return "$flag $name: $cc ($prefix)"
};
my &find-pos = sub (MultiT $result, Int:D $pos, @array --> Int:D) {
    for @array.kv -> $idx, %r {
        if %r{$id-name} == $result {
            $pos = $idx;
            last; # found so don't waste resources #
        }
    }
    return $pos;
}
my &get-result = sub (MultiT:D $result, Int:D $pos, Int:D $length, @array --> MultiT:D ) {
    my $res = $result;
    if $pos ~~ 0..^$length {
      my %row = |%(@array[$pos]);
      $res = %row«id» if %row«id»:exists;
    }
    return $res
};
my Int:D $cc-id        = dropdown($cc_id, 20, 'id',
                                    &setup-option-str, &find-pos, &get-result, @_country);
while !valid-country-cc-id($cc-id, %countries) {
    $cc-id             = dropdown($cc-id, 20, 'id',
                                    &setup-option-str, &find-pos, &get-result, @_country);
}

=end code

Or using a much simpler array.
B<NB: from C<menu>>

L<Top of Document|#table-of-contents>

=begin code :lang<raku>

my &setup-option-str = sub (Int:D $cnt, Int:D $pos, @array --> Str:D ) {
    return @array[$cnt];
};
my &get-result = sub (MultiT:D $result, Int:D $pos, Int:D $length, @array --> MultiT:D ) {
    my $res = $result;
    if $pos ~~ 0..^$length {
      $res = @array[$pos];
    }
    return $res
};
my &find-pos = sub (MultiT $result, Int:D $pos, @array --> Int:D) {
    for @array.kv -> $idx, $r {
        if $r eq $result {
            $pos = $idx;
            last; # found so don't waste resources #
        }
    }
    return $pos;
}
my Str:D $result = dropdown(@candidates[@candidates.elems - 1], 40, 'backup',
                                    &setup-option-str, &find-pos, &get-result, @candidates);

=end code

L<Top of Document|#table-of-contents>

=end pod

###############################################################################################################
#                                                                                                             #
#       Emulates dropdown/list behaviour as best you can on a terminal. not a real dropdown always down!!!    #
#                                                                                                             #
###############################################################################################################

sub normalise_top(Int:D $top is copy, Int:D $pos, Int:D $window-height, Int:D $length --> Int:D) {
    $top = $pos - $window-height div 2 if $pos < $top;
    $top = $pos - $window-height div 2 if $pos >= $top + $window-height;
    $top = $length - $window-height if $top + $window-height >= $length;
    $top = 0 if $top < 0;
    return $top;
} # sub normalise_top(Int:D $top is copy, Int:D $pos, Int:D $window-height, Int:D $length --> Int:D) #

sub dropdown(MultiT:D $id, Int:D $window-height is copy, Str:D $id-name,
                        &setup-option-str:(Int:D $c, Int:D $p, @a --> Str:D),
                        &find-pos:(MultiT $r, Int:D $p, @a --> Int:D),
                        &get-result:(MultiT:D $res, Int:D $p, Int:D $l, @a --> MultiT:D),
                        @array,
                        Str:D :$highlight-bg-colour = t.bg-color(0, 0, 127) ~ t.bold, 
                        Str:D :$highlight-fg-colour = t.bright-yellow, 
                        Str:D :$bg-colour0 = t.bg-yellow ~ t.bold, 
                        Str:D :$fg-colour0 = t.bright-blue, 
                        Str:D :$bg-colour1 = t.bg-color(0, 127, 0) ~ t.bold, 
                        Str:D :$fg-colour1 = t.bright-blue,  
                        Str:D :$bg-prompt = t.bg-green ~ t.bold, 
                        Str:D :$fg-prompt = t.bright-blue, 
                        Bool:D :$wrap-around = False --> MultiT) is export  {
    put t.save-screen;
    my MultiT $result = $id;
    try {
        my Int:D $pos    = -1;
        my Int:D $top    = -1;
        my Str:D $bgcolour = '';
        my Str:D $fgcolour = '';
        my Int:D $length = @array.elems;
        $window-height = $length if $window-height > $length;
        $pos = &find-pos($result, $pos, @array);
        $top = normalise_top($top, $pos, $window-height, $length);
        my Str $key;
        my $original-flags := Term::termios.new(:fd($*IN.native-descriptor)).getattr;
        @signal.push: {
            $original-flags.setattr(:NOW);
            print t.show-cursor ~ t.restore-screen;
        };
        #«««
        my $flags := Term::termios.new(:fd($*IN.native-descriptor)).getattr;
        $flags.unset_lflags('ICANON');
        $flags.unset_lflags('ECHO');
        $flags.setattr(:NOW);
        #»»»
        my Int $width = terminal-width;
        $width = 80 if $width === Int;
        my Int:D $m = 0;
        loop (my Int $i = 0; $i < $length; $i++) {
            $m = max($m, hwcswidth(&setup-option-str($i, $pos, @array)));
        } # loop (my Int $i = 0; $i < $length; $i++) #
        $m = max($m, hwcswidth('use up and down arrows or page up and down : and enter to select (esc, q or Q to quit)'));
        my Int:D $w = min($width, $m + 2);
        put t.hide-cursor;
        loop {
            put t.clear-screen;
            loop (my Int $cnt = $top; $cnt < $top + $window-height && $cnt < $length; $cnt++) {
                if $cnt == $pos {
                    $bgcolour = $highlight-bg-colour;
                    $fgcolour = $highlight-fg-colour;
                } elsif $cnt % 2 == 0 {
                    $bgcolour = $bg-colour0;
                    $fgcolour = $fg-colour0;
                } else {
                    $bgcolour = $bg-colour1;
                    $fgcolour = $fg-colour1;
                }
                put $bgcolour ~ $fgcolour ~ Sprintf("%-*s", $w, &setup-option-str($cnt, $pos, @array)) ~ t.text-reset;
            } # loop (my Int $cnt = $top; $cnt <= $top + $window-height; $cnt++) #
            $cnt = $top + $window-height;
            my Int:D $wdth = hwcswidth(trailing-dots('use up and down arrows or page up and down', 42));
            put $bg-prompt ~ $fg-prompt ~ Sprintf("%-*s: %-*s", $wdth, trailing-dots('use up and down arrows or page up and down', 42),
                                                          $w - $wdth - hwcswidth(': '), 'and enter to select (esc, q or Q to quit)') ~ t.text-reset;
            $cnt++;
            $key = $*IN.read(10).decode;
            given $key {
                when 'k'          {
                    $pos--;
                    if $wrap-around {
                        $pos = ($length - 1) if $pos < 0;
                    } else {
                        $pos = 0 if $pos < 0;
                    }
                    $top-- if $pos < $top;
                    $top = normalise_top($top, $pos, $window-height, $length);
                } # up #
                when 'j'          {
                    $pos++;
                    if $wrap-around {
                        $pos = 0 if $pos >= $length;
                    } else {
                        $pos = ($length - 1) if $pos >= $length;
                    }
                    $top++ if $pos >= $top + $window-height;
                    $top = normalise_top($top, $pos, $window-height, $length);
                } # down #
                when "\x[1B][A"   {
                    $pos--;
                    if $wrap-around {
                        $pos = ($length - 1) if $pos < 0;
                    } else {
                        $pos = 0 if $pos < 0;
                    }
                    $top-- if $pos < $top;
                    $top = normalise_top($top, $pos, $window-height, $length);
                } # up #
                when "\x[1B][B"   {
                    $pos++;
                    if $wrap-around {
                        $pos = 0 if $pos >= $length;
                    } else {
                        $pos = ($length - 1) if $pos >= $length;
                    }
                    $top++ if $pos >= $top + $window-height;
                    $top = normalise_top($top, $pos, $window-height, $length);
                } # down #
                when "\x[1B][5~"  {
                    $pos -= ($window-height - 1);
                    if $wrap-around {
                        $pos = ($length - 1) if $pos < 0;
                    } else {
                        $pos = 0 if $pos < 0;
                    }
                    $top -= ($window-height - 1) if $pos < $top;
                    $top = normalise_top($top, $pos, $window-height, $length);
                } # page up #
                when "\x[1B][6~"  {
                    $pos += ($window-height - 1);
                    if $wrap-around {
                        $pos = 0 if $pos >= $length;
                    } else {
                        $pos = ($length - 1) if $pos >= $length;
                    }
                    $top = normalise_top($top, $pos, $window-height, $length);
                } # page down #
                when "\x[1B][H"   {
                    $pos = 0;
                    $top = 0;
                    $top = normalise_top($top, $pos, $window-height, $length);
                } # home #
                when "\x[1B][F"   {
                    $pos = ($length - 1);
                    $top = $length - $window-height;
                    $top = normalise_top($top, $pos, $window-height, $length);
                } # end #
                when "\x[1B]"     { last; } # esc #
                when "\n"         {   # enter #
                                      #`«««
                                      if $pos ~~ 0..^$length {
                                          my %row = |%(@array[$pos]);
                                          $result = %row{$id-name} if %row{$id-name}:exists;
                                      }
                                      # »»»
                                      $result = &get-result($result, $pos, $length, @array);
                                      last;
                                  }
                when "q"         { last; } # quit #
                when "Q"         { last; } # quit #
            }
            $*ERR.say: '=' x 80;
            $*ERR.flush;
        } # loop #
        $original-flags.setattr(:NOW);
        @signal.pop if @signal;
        CATCH {
            default {
                .backtrace;
                .Str.say;
                $original-flags.setattr(:NOW);
                @signal.pop if @signal;
                print t.show-cursor ~ t.restore