### ----------------------------------------------------
### -- Gzz::Text::Utils
### -- Licenses: LGPL-3.0-or-later
### -- Authors: Francis Grizzly Smit <grizzly@smit.id.au>
### -- File: rakulib/Gzz/Text/Utils.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 8
 %cursor-left if $debug;
        make %cursor-left;
    }
    method cursor-next-line($/) {
        my %cursor-next-line = type => 'ansi', sub-type => 'cursor-next-line', val => ~$/;
        dd %cursor-next-line if $debug;
        make %cursor-next-line;
    }
    method cursor-prev-line($/) {
        my %cursor-prev-line = type => 'ansi', sub-type => 'cursor-prev-line', val => ~$/;
        dd %cursor-prev-line if $debug;
        make %cursor-prev-line;
    }
    method print-at($/) {
        my %print-at = type => 'ansi', sub-type => 'print-at', val => ~$/;
        dd %print-at if $debug;
        make %print-at;
    }
    method set-fg-color($/) {
        my %set-fg-color = type => 'ansi', sub-type => 'set-fg-color', val => ~$/;
        dd %set-fg-color if $debug;
        make %set-fg-color;
    }
    method set-fg-rgb-color($/) {
        my %set-fg-rgb-color = type => 'ansi', sub-type => 'set-fg-rgb-color', val => ~$/;
        dd %set-fg-rgb-color if $debug;
        make %set-fg-rgb-color;
    }
    method set-bg-color($/) {
        my %set-bg-color = type => 'ansi', sub-type => 'set-bg-color', val => ~$/;
        dd %set-bg-color if $debug;
        make %set-bg-color;
    }
    method set-bg-rgb-color($/) {
        my %set-bg-rgb-color = type => 'ansi', sub-type => 'set-bg-rgb-color', val => ~$/;
        dd %set-bg-rgb-color if $debug;
        make %set-bg-rgb-color;
    }
    method set-bg-default($/) {
        my %set-bg-default = type => 'ansi', sub-type => 'set-bg-default', val => ~$/;
        dd %set-bg-default if $debug;
        make %set-bg-default;
    }
    method save-cursor($/) {
        my %save-cursor = type => 'ansi', sub-type => 'save-cursor', val => ~$/;
        dd %save-cursor if $debug;
        make %save-cursor;
    }
    method restore-cursor($/) {
        my %restore-cursor = type => 'ansi', sub-type => 'restore-cursor', val => ~$/;
        dd %restore-cursor if $debug;
        make %restore-cursor;
    }
    method start-of-line($/) {
        my %start-of-line = type => 'ansi', sub-type => 'start-of-line', val => ~$/;
        dd %start-of-line if $debug;
        make %start-of-line;
    }
    method erase-to-end-of-line($/) {
        my %erase-to-end-of-line = type => 'ansi', sub-type => 'erase-to-end-of-line', val => ~$/;
        dd %erase-to-end-of-line if $debug;
        make %erase-to-end-of-line;
    }
    method normal-video($/) {
        my %normal-video = type => 'ansi', sub-type => 'normal-video', val => ~$/;
        dd %normal-video if $debug;
        make %normal-video;
    }
    method bold($/) {
        my %bold = type => 'ansi', sub-type => 'bold', val => ~$/;
        dd %bold if $debug;
        make %bold;
    }
    method faint($/) {
        my %faint = type => 'ansi', sub-type => 'faint', val => ~$/;
        dd %faint if $debug;
        make %faint;
    }
    method italic($/) {
        my %italic = type => 'ansi', sub-type => 'italic', val => ~$/;
        dd %italic if $debug;
        make %italic;
    }
    method underline($/) {
        my %underline = type => 'ansi', sub-type => 'underline', val => ~$/;
        dd %underline if $debug;
        make %underline;
    }
    method blink($/) {
        my %blink = type => 'ansi', sub-type => 'blink', val => ~$/;
        dd %blink if $debug;
        make %blink;
    }
    method reverse-video($/) {
        my %reverse-video = type => 'ansi', sub-type => 'reverse-video', val => ~$/;
        dd %reverse-video if $debug;
        make %reverse-video;
    }
    method strike($/) {
        my %strike = type => 'ansi', sub-type => 'strike', val => ~$/;
        dd %strike if $debug;
        make %strike;
    }
    method alt-font($/) {
        my %alt-font = type => 'ansi', sub-type => 'alt-font', val => ~$/;
        dd %alt-font if $debug;
        make %alt-font;
    }
    method ansi($/) {
        my %ansi;
        if $/<clear-screen> {
            %ansi = $/<clear-screen>.made;
        } elsif $/<home> {
            %ansi = $/<home>.made;
        } elsif $/<move-to> {
            %ansi = $/<move-to>.made;
        } elsif $/<reset-scroll-region> {
            %ansi = $/<reset-scroll-region>.made;
        } elsif $/<set-scroll-region> {
            %ansi = $/<set-scroll-region>.made;
        } elsif $/<scroll-down> {
            %ansi = $/<scroll-down>.made;
        } elsif $/<scroll-up> {
            %ansi = $/<scroll-up>.made;
        } elsif $/<hide-cursor> {
            %ansi = $/<hide-cursor>.made;
        } elsif $/<save-screen> {
            %ansi = $/<save-screen>.made;
        } elsif $/<restore-screen> {
            %ansi = $/<restore-screen>.made;
        } elsif $/<show-cursor> {
            %ansi = $/<show-cursor>.made;
        } elsif $/<cursor-up> {
            %ansi = $/<cursor-up>.made;
        } elsif $/<cursor-down> {
            %ansi = $/<cursor-down>.made;
        } elsif $/<cursor-right> {
            %ansi = $/<cursor-right>.made;
        } elsif $/<cursor-left> {
            %ansi = $/<cursor-left>.made;
        } elsif $/<cursor-next-line> {
            %ansi = $/<cursor-next-line>.made;
        } elsif $/<cursor-prev-line> {
            %ansi = $/<cursor-prev-line>.made;
        } elsif $/<print-at> {
            %ansi = $/<print-at>.made;
        } elsif $/<set-fg-color> {
            %ansi = $/<set-fg-color>.made;
        } elsif $/<set-fg-rgb-color> {
            %ansi = $/<set-fg-rgb-color>.made;
        } elsif $/<set-bg-color> {
            %ansi = $/<set-bg-color>.made;
        } elsif $/<set-bg-rgb-color> {
            %ansi = $/<set-bg-rgb-color>.made;
        } elsif $/<set-bg-default> {
            %ansi = $/<set-bg-default>.made;
        } elsif $/<save-cursor> {
            %ansi = $/<save-cursor>.made;
        } elsif $/<restore-cursor> {
            %ansi = $/<restore-cursor>.made;
        } elsif $/<start-of-line> {
            %ansi = $/<start-of-line>.made;
        } elsif $/<erase-to-end-of-line> {
            %ansi = $/<erase-to-end-of-line>.made;
        } elsif $/<normal-video> {
            %ansi = $/<normal-video>.made;
        } elsif $/<bold> {
            %ansi = $/<bold>.made;
        } elsif $/<faint> {
            %ansi = $/<faint>.made;
        } elsif $/<italic> {
            %ansi = $/<italic>.made;
        } elsif $/<underline> {
            %ansi = $/<underline>.made;
        } elsif $/<blink> {
            %ansi = $/<blink>.made;
        } elsif $/<reverse-video> {
            %ansi = $/<reverse-video>.made;
        } elsif $/<strike> {
            %ansi = $/<strike>.made;
        } elsif $/<alt-font> {
            %ansi = $/<alt-font>.made;
        }
        dd %ansi if $debug;
        make %ansi;
    }
    method chunk($/) {
        my %chunk = type => 'chunk', sub-type => 'chunk', val => ~$/;
        dd %chunk if $debug;
        make %chunk;
    }
    method chunks($/) {
        my %chunks;
        if $/<chunk> {
            %chunks = $/<chunk>.made;
        } elsif $/<ansi> {
            %chunks = $/<ansi>.made;
        }
        dd %chunks if $debug;
        make %chunks;
    }
    #token empty          { '' }
    method empty($/) {
        my %empty = type => 'chunk', sub-type => 'empty', val => ~$/;
        dd %empty if $debug;
        make %empty;
    }
    #token block          { <chunks>+ }
    method block($/) {
        my @block = $/<chunks>».made;
        dd @block if $debug;
        make @block;
    }
    #token text           { ^ [  <block> || <empty> ] $ }
    method text($/) {
        my @text;
        if $/<block> {
            @text = $/<block>.made;
        } elsif $/<empty> {
            my %empt = $/<empty>.made;
            @text.push(%empt);
        }
        dd @text if $debug;
        make @text;
    }
} # role UnhighlightBaseActions #

grammar Unhighlight is UnhighlightBase {
    token TOP    { <text> }
}

class UnhighlightActions does UnhighlightBaseActions {
    method TOP($made) {
        my @top = $made<text>.made;
        dd @top if $debug;
        $made.make: @top;
    }
} # class UnhighlightActions does UnhighlightBaseActions #

=begin pod

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

=head1 The Functions Provided

=begin item 

strip-ansi

=begin code :lang<raku>

sub strip-ansi(Str:D $text --> Str:D) is export

=end code

Strips out all the ANSI escapes, at the moment just those provided by the B<C<Terminal::ANSI>> 
or B<C<Terminal::ANSI::OO>> modules both available as B<C<Terminal::ANSI>> from zef etc I am not sure
how exhaustive that is,  but I will implement any more escapes as I become aware of them.

=end item

=end pod

sub strip-ansi(Str:D $text --> Str:D) is export {
    dd $text if $debug;
    #return $text if $text.trim eq '';
    my $actions = UnhighlightActions;
    my @stuff = Unhighlight.parse($text, :enc('UTF-8'), :$actions).made;
    my @cleaned = @stuff.grep( -> %chnk { %chnk«type» eq 'chunk' }).map: -> %chk { %chk«val» };
    return @cleaned.join();
} # sub strip-ansi(Str:D $text --> Str:D) is export #

=begin pod

=begin item

hwcswidth
=begin code :lang<raku>

sub hwcswidth(Str:D $text --> Int:D) is export

=end code

Same as B<C<wcswidth>> but it copes with ANSI escape sequences unlike B<C<wcswidth>>.

=end item

=begin item2

The secret sauce is that it is defined as:

=begin code :lang<raku>

sub hwcswidth(Str:D $text --> Int:D) is export {
    return wcswidth(strip-ansi($text));
} #  sub hwcswidth(Str:D $text --> Int:D) is export #


=end code

=end item2

=end pod

sub hwcswidth(Str:D $text --> Int:D) is export {
    return wcswidth(strip-ansi($text));
} #  sub hwcswidth(Str:D $text --> Int:D) is export #

=begin pod

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

=begin head3

Here are 4 functions provided  to B<C<centre>>, B<C<left>> and B<C<right>> justify text even when
it is ANSI formatted.

=end head3

=head3 centre

=item ⋄

=begin item2

Centring text in a field.

=begin code :lang<raku>

sub centre(Str:D $text,
           Int:D $width is copy,
           Str:D $fill = ' ',
           :&number-of-chars:(Int:D, Int:D --> Bool:D) =
                               &centre-global-number-of-chars,
           Str:D :$ref = strip-ansi($text),
           Int:D :$max-width = 0,
           Str:D :$ellipsis = '' --> Str) is export {

=end code

=end item2

=begin item4

Centres the text B<C<$text>> in a field of width B<C<$width>> padding either side with B<C<$fill>>

=end item4

=begin item4

B<Where:>

=end item4

=begin item5

B<C<$fill>>      is the fill char by default B<C<$fill>> is set to a single white space.

=end item5

=begin item6

If  it requires an odd number of padding then the right hand side will get one more char/codepoint.

=end item6

=begin item5

B<C<&number-of-chars>> takes a function which takes 2 B<C<Int:D>>'s and returns a B<C<Bool:D>>.

=end item5

=begin item6

By default this is equal to the closure B<C<centre-global-number-of-chars>> which looks like:

=begin code :lang<raku>

our $centre-total-number-of-chars is export = 0;
our $centre-total-number-of-visible-chars
                                  is export = 0;

sub centre-global-number-of-chars(
      Int:D $number-of-chars,
      Int:D $number-of-visible-chars --> Bool:D) {
    $centre-total-number-of-chars         =
                         $number-of-chars;
    $centre-total-number-of-visible-chars =
                         $number-of-visible-chars;
    return True;
}

=end code

=end item6

=begin item7 

Which is a closure around the variables: B<C<$centre-total-number-of-chars>> and B<C<$centre-total-number-of-visible-chars>>, 
these are global B<C<our>> variables that B<C<Gzz::Text::Utils>> exports.
But you can just use B<C<my>> variables from with a scope, just as well. And make the B<C<sub>> local to the same scope.

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

i.e.

=begin code :lang<raku>
    
sub Sprintf(Str:D $format-str,
                :&number-of-chars:(Int:D, Int:D --> Bool:D) = &Sprintf-global-number-of-chars,
                                                        Str:D :$ellipsis = '', *@args --> Str) is export {
    ...
    ...
    ...
    my Int:D $total-number-of-chars = 0;
    my Int:D $total-number-of-visible-chars = 0;
    sub internal-number-of-chars(Int:D $number-of-chars, Int:D $number-of-visible-chars --> Bool:D) {
        $total-number-of-chars += $number-of-chars;
        $total-number-of-visible-chars += $number-of-visible-chars;
        return True;
    } # sub internal-number-of-chars(Int:D $number-of-chars, Int:D $number-of-visible-chars --> Bool:D) #
    ...
    ...
    ...
    for @format-str -> %elt {
        my Str:D $type = %elt«type»;
        if $type eq 'literal' {
            my Str:D $lit = %elt«val»;
            $total-number-of-chars += $lit.chars;
            $total-number-of-visible-chars += strip-ansi($lit).chars;
            $result ~= $lit;
        } elsif $type eq 'fmt-spec' {
            ...
            ...
            ...
            given $spec-char {
                when 'c' {
                             $arg .=Str;
                             $ref .=Str;
                             BadArg.new(:msg("arg should be one codepoint: {$arg.codes} found")).throw if $arg.codes != 1;
                             $max-width = max($max-width, $precision, 0) if $max-width > 0; #`« should not really have a both for this
                                                                                                so munge together.
                                                                                                Traditionally sprintf etc treat precision
                                                                                                as max-width for strings. »
                             if $padding eq '' {
                                 if $justify eq '' {
                                     $result ~=  right($arg, $width, :$ref, :number-of-chars(&internal-number-of-chars), :$max-width);
                                 } elsif $justify eq '-' {
                                     $result ~=  left($arg, $width, :$ref, :number-of-chars(&internal-number-of-chars), :$max-width);
                                 } elsif $justify eq '^' {
                                     $result ~=  centre($arg, $width, :$ref, :number-of-chars(&internal-number-of-chars), :$max-width);
                                 }
                             } else {
                                 if $justify eq '' {
                                     $result ~=  right($arg, $width, $padding, :$ref, :number-of-chars(&internal-number-of-chars), :$max-width);
                                 } elsif $justify eq '-' {
                                     $result ~=  left($arg, $width, $padding, :$ref, :number-of-chars(&internal-number-of-chars), :$max-width);
                                 } elsif $justify eq '^' {
                                     $result ~=  centre($arg, $width, $padding, :$ref, :number-of-chars(&internal-number-of-chars), :$max-width);
                                 }
                             }
                         }
                when 's' {
                            ...
                            ...
                            ...
        ...
        ...
        ...
    ...
    ...
    ...
    return $result;
    KEEP {
        &number-of-chars($total-number-of-chars, $total-number-of-visible-chars);
    }
} #`««« sub Sprintf(Str:D $format-str,
                :&number-of-chars:(Int:D, Int:D --> Bool:D) = &Sprintf-global-number-of-chars,
                                                        Str:D :$ellipsis = '', *@args --> Str) is export »»»
 
=end code

=end item7

=begin item5

The parameter B<C<:$ref>> is by default set to the value of B<C<strip-ansi($text)>>
=end item5

=begin item6

This is used to obtain the length of the of the text using B<I<C<wcswidth(Str)>>> from module B<"C<Terminal::WCWidth>">
which is used to obtain the width the text if printed on the current terminal:

=end item6

=begin item7

B<NB: C<wcswidth> will return -1 if you pass it text with colours etc embedded in them>.

=end item7

=begin item7

B<"C<Terminal::WCWidth>"> is witten by B<bluebear94> L<github:bluebear94|https://raku.land/github:bluebear94> get it with B<zef> or whatever

=end item7

=begin item5

B<C<:$max-width>> sets the maximum width of the field but if set to B<C<0>> (The default), will effectively be infinite (∞).

=end item5

=begin item5

B<C<:$ellipsis>> is used to elide the text if it's too big I recommend either B<C<''>> the default or B<C<'…'>>.

=end item5

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

=head3 left

=item ⋄

=begin item2

Left Justifying text.

=begin code :lang<raku>

sub left(Str:D $text, Int:D $width is copy, Str:D $fill = ' ',
        :&number-of-chars:(Int:D, Int:D --> Bool:D) = &left-global-number-of-chars,
                    Str:D :$ref = strip-ansi($text), Int:D :$max-width = 0,
                                        Str:D :$ellipsis = '' --> Str) is export

=end code

=end item2

=item3       B<C<left>> is the same except that except that it puts all the  padding on the right of the field.

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

=head3 right

=item ⋄

=begin item2

Right justifying text.

=begin code :lang<raku>

sub right(Str:D $text, Int:D $width is copy, Str:D $fill = ' ',
          :&number-of-chars:(Int:D, Int:D --> Bool:D) = &right-global-number-of-chars,
                    Str:D :$ref = strip-ansi($text), Int:D :$max-width = 0,
                                             Str:D :$ellipsis = '' --> Str) is export

=end code

=end item2





=item3       B<C<right>> is again the same except it puts all the padding on the left and the text to the right.

=begin item3
L<Top of Document|#table-of-contents>

=head4 crop-field

=end item3
=item2 text

=begin item3 

Cropping Text in a field.

=begin code :lang<raku>

sub crop-field(Str:D $text,
               Int:D $w is rw,
               Int:D $width is rw,
               Bool:D $cropped is rw,
               Int:D $max-width,
               Str:D :$ellipsis = '' --> Str:D) is export {

=end code

=end item3

=begin item4

B<C<crop-field>> used by B<C<centre>>, B<C<left>> and B<C<right>> to crop their input if necessary. Copes with
ANSI escape codes.

=end item4

=begin item5

B<Where>

=end item5

=begin item6 

B<C<$text>> is the text to be cropped possibly, wit ANSI escapes embedded. 

=end item6

=begin item6

B<C<$w>> is used to hold the width of B<C<$text>> is read-write so will return that value.

=end item6

=begin item6 

B<C<$width>> is the desired width. Will be used to return the updated width.

=end item6

=begin item6 

B<C<$cropped>> is used to return the status of whether or not B<C<$text>> was truncated.

=end item6

=begin item6 

B<C<$max-width>> is the maximum width we are allowing.

=end item6

=begin item6 

B<C<$ellipsis>> is used to supply a eliding . Empty string by default.

=end item6

=end pod




sub crop-field(Str:D $text,
               Int:D $w is rw,
               Int:D $width is rw,
               Bool:D $cropped is rw,
               Int:D $max-width,
               Str:D :$ellipsis = '' --> Str:D) is export {
    if $debug {
        my $line = "$?FILE\[$?LINE] {$?MODULE.gist} {&?ROUTINE.signature.gist}";
        dd $w, $max-width, $text, $line;
    }
    if $max-width > 0 {
        $w  = hwcswidth($text); # just in case $w wasn't set correctly on call #
        if $w > $max-width {
            my $actions = UnhighlightActions;
            my @chunks = Unhighlight.parse($text, :enc('UTF-8'), :$actions).made;
            my Str:D $tmp = '';
            if $debug {
                my Str:D $line = "$?FILE\[$?LINE] {$?MODULE.gist} {&?ROUTINE.signature.gist}";
                dd @chunks, $w, $max-width, $tmp, $line;
            }
            $w = 0;
            my %chunk;
            my Int:D $i = -1;
            loop ($i = 0; $i < @chunks.elems; $i++) {
                %chunk = @chunks[$i];
                $w = hwcswidth($tmp ~ %chunk«val» ~ $ellipsis);
                if $w > $max-width {
                    last;
                }
                $tmp ~= %chunk«val»;
                if $debug {
                    my Str:D $line = "\[$?LINE]";
                    dd @chunks, $w, $max-width, $tmp, $line;
                }
            }
            if hwcswidth($tmp) < $max-width {
                while %chunk«type» ne 'chunk' && $i < @chunks.elems {
                    if $debug {
                        my $t = @chunks[$i];
                        dd @chunks, $i, $t, %chunk;
                    }
                    %chunk = @chunks[$i];
                    $i++;
                }
                $w = hwcswidth($tmp ~ %chunk«val» ~ $ellipsis);
                my Str:D $val = '';
                if %chunk«type» eq 'chunk' {
                    $val = %chunk«val»;
                } else {
                    $cropped = True;
                    $tmp ~= "\e[0m";
                    return $tmp;
                }
                while hwcswidth($tmp ~ $val ~ $ellipsis) > $max-width {
                    last if $val eq '';
                    $val = $val.substr(0, *-1);
                }
                $tmp ~= $val ~ $ellipsis ~ "\e[0m";
                $cropped = True;
                $width = $max-width if $width > $max-width;
                return $tmp;
            } # if hwcswidth($tmp) < $max-width #
            if $debug {
                my $line = "$?FILE\[$?LINE] {$?MODULE.gist} {&?ROUTINE.signature.gist}";
                dd @chunks, $w, $max-width, $tmp, $line;
            }
            if $i + 1 < @chunks.elems {
                $tmp ~= $ellipsis ~ "\e[0m";
                $cropped = True;
                $width = $max-width if $width > $max-width;
                return $tmp;
            }
        } # if $w > $max-width #
        $width = $max-width if $width > $max-width;
    } # if $max-width > 0 #
    $w  = hwcswidth($text); # insure that $w is set correctly #
    $cropped = False;
    return $text;
} #`««« sub crop-field(Str:D $text,
               Int:D $w is rw,
               Int:D $width is rw,
               Bool:D $cropped is rw,
               Int:D $max-width,
               Str:D :$ellipsis = '' --> Str:D) is export »»»

our $centre-total-number-of-chars is export = 0;
our $centre-total-number-of-visible-chars is export = 0;

sub centre-global-number-of-chars(Int:D $number-of-chars, Int:D $number-of-visible-chars --> Bool:D) {
    $centre-total-number-of-chars         = $number-of-chars;
    $centre-total-number-of-visible-chars = $number-of-visible-chars;
    return True;
}

our $left-total-number-of-chars is export = 0;
our $left-total-number-of-visible-chars is export = 0;

sub left-global-number-of-chars(Int:D $number-of-chars, Int:D $number-of-visible-chars --> Bool:D) {
    $left-total-number-of-chars         = $number-of-chars;
    $left-total-number-of-visible-chars = $number-of-visible-chars;
    return True
}

our $right-total-number-of-chars is export = 0;
our $right-total-number-of-visible-chars is export = 0;

sub right-global-number-of-chars(Int:D $number-of-chars, Int:D $number-of-visible-chars --> Bool:D) {
    $right-total-number-of-chars         = $number-of-chars;
    $right-total-number-of-visible-chars = $number-of-visible-chars;
    return True
}


sub centre(Str:D $text,
           Int:D $width is copy,
           Str:D $fill = ' ',
           :&number-of-chars:(Int:D, Int:D --> Bool:D) =
                               &centre-global-number-of-chars,
           Str:D :$ref = strip-ansi($text),
           Int:D :$max-width = 0,
           Str:D :$ellipsis = '' --> Str) is export {
    my Int:D $w  = wcswidth($ref);
    dd $w, $width, $max-width, $text, $ref if $debug;
    my Bool:D $cropped = False;
    my Str $result = crop-field($text, $w, $width,  $cropped, $max-width, :$ellipsis);
    return $result if $cropped;
    return $result if $w < 0;
    return $result if $width <= $w;
    $width -= $w;
    return $result if $width <= 0;
    my Int:D $fill-width = wcswidth($fill);
    $fill-width = 1 unless $fill-width > 0;
    my Int:D $extra-width = $width % $fill-width;
    my Int:D $left-extra-width = $extra-width div 2;
    $extra-width -= $left-extra-width;
    $width div= $fill-width;
    my Int:D $l  = $width div 2;
    $result = $fill x $l ~ $fill.substr(0, $left-extra-width) ~ $result ~ $fill x ($width - $l) ~ $fill.substr(0, $extra-width);
    return $result;
    KEEP {
        &number-of-chars($result.chars, strip-ansi($result).chars);
    }
} #`««« sub centre(Str:D $text, Int:D $width is copy, Str:D $fill = ' ',
                    :&number-of-chars:(Int:D, Int:D --> Bool:D) = &centre-global-number-of-chars,
                        Str:D :$ref = strip-ansi($text), Int:D :$max-width = 0, Str:D :$ellipsis = '' --> Str) is export »»»

sub left(Str:D $text, Int:D $width is copy, Str:D $fill = ' ',
                :&number-of-chars:(Int:D, Int:D --> Bool:D) = &left-global-number-of-chars,
                    Str:D :$ref = strip-ansi($text),
                    Int:D :$max-width = 0,
                    Str:D :$ellipsis = '' --> Str) is export {
    my Int:D $w  = wcswidth($ref);
    dd $w, $width, $max-width, $text, $ref if $debug;
    my Bool:D $cropped = False;
    my Str $result = crop-field($text, $w, $width,  $cropped, $max-width, :$ellipsis);
    return $result if $cropped;
    return $result if $w < 0;
    return $result if $width <= 0;
    return $result if $width <= $w;
    my Int:D $l  = ($width - $w).abs;
    my Int:D $fill-width = wcswidth($fill);
    $fill-width = 1 unless $fill-width > 0;
    my Int:D $extra-width = $l % $fill-width;
    $l div= $fill-width;
    $result ~= $fill x $l ~ $fill.substr(0, $extra-width);
    return $result;
    KEEP {
        &number-of-chars($result.chars, strip-ansi($result).chars);
    }
} #`««« sub left(Str:D $text, Int:D $width is copy, Str:D $fill = ' ',
                    :&number-of-chars:(Int:D, Int:D --> Bool:D) = &left-global-number-of-chars,
                        Str:D :$ref = strip-ansi($text), Int:D :$max-width = 0, Str:D :$ellipsis = '' --> Str) is export »»»

sub right(Str:D $text, Int:D $width is copy, Str:D $fill = ' ',
                    :&number-of-chars:(Int:D, Int:D --> Bool:D) = &right-global-number-of-chars,
                        Str:D :$ref = strip-ansi($text), Int:D :$max-width = 0, Str:D :$ellipsis = '' --> Str) is export {
    my Int:D $w  = wcswidth($ref);
    my Bool:D $cropped = False;
    my Str $result = crop-field($text, $w, $width,  $cropped, $max-width, :$ellipsis);
    return $result if $cropped;
    return $result if $w < 0;
    return $result if $width <= 0;
    return $result if $width <= $w;
    my Int:D $l  = $width - $w;
    my Int:D $fill-width = wcswidth($fill);
    $fill-width = 1 unless $fill-width > 0;
    my Int:D $extra-width = $l % $fill-width;
    $l div= $fill-width;
    dd $l, $result if $debug;
    dd $w, $width, $max-width, $result, $ref, $fill if $debug;
    $result = ($fill x $l) ~ $fill.substr(0, $extra-width) ~ $result;
    return $result;
    KEEP {
        &number-of-chars($result.chars, strip-ansi($result).chars);
    }
} #`««« sub right(Str:D $text, Int:D $width is copy, Str:D $fill = ' ',
                    :&number-of-chars:(Int:D, Int:D --> Bool:D) = &right-global-number-of-chars, Str:D
                        :$ref = strip-ansi($text), Int:D :$max-width = 0, Str:D :$ellipsis = '' --> Str) is export »»»

=begin pod

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

=head2 Sprintf

=begin item

Sprintf like sprintf only it can deal with ANSI highlighted text. And has lots of other options, including the ability
to specify a B<C<$max-width>> using B<C<width.precision.max-width>>, which can be B<C<.*>>, B«C«*<num>$»», B«C«.*»»,  or B«C«<num>»»

=begin code :lang<raku>

sub Sprintf(Str:D $format-str,
           :&number-of-chars:(Int:D, Int:D --> Bool:D) =
                                             &Sprintf-global-number-of-chars,
                                             Str:D :$ellipsis = '',
                                             *@args --> Str) is export 

=end code

=end item

=begin item2

Where:

=end item2

=begin item3

B<C<format-str>> is is a superset of the B<C<sprintf>> format string,  but it has extra features: 
like the flag B<C<[ <char> ]>> where <char> can be almost anything except B<C<[>>, B<C<]>> B<control characters>, 
B<white space other than the normal space>, and B<C<max-width>> after the precision.

=end item3

=begin item4

The format string looks like this:                                                        

=begin  code :lang<raku>

token format      { <chunks>+ }
token chunks      { [ <chunk> || '%' <format-spec> ] }
token chunk       { <-[%]>+ }
token format-spec { [ <fmt-esc> || <fmt-spec> ] }
token fmt-esc     { [      '%' #`« a literal % »
                        || 'N' #`« a nl i.e. \n char but does not require interpolation so no double quotes required »
                        || 'T' #`« a tab i.e. \t char but does not require interpolation so no double quotes required »
                        || 'n' #`« not implemented and will not be, throws an exception if matched »
                        || 't' #`« not implemented and will not be, throws an exception if matched »
                    ]
                  }
token fmt-spec   { [ <dollar-directive> '$' ]? <flags>?  <width>? [ '.' <precision> [ '.' <max-width> ]? ]? <modifier>? <spec-char> }


=end code


=end item4

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


=begin item5

Where

=end item5

=begin item6

B<C<dollar-directive>> is a integer >= 1

=end item6                        

=begin item6

B<C<flags>> is any zero or more of:

=end item6                        

=begin item7

B<C<+>> put a plus in front of positive values.

=end item7                        


=begin item7

B<C