### ----------------------------------------------------
### -- Gzz::Text::Utils
### -- Licenses: LGPL-3.0-or-later
### -- Authors: Francis Grizzly Smit <grizzly@smit.id.au>
### -- File: rakulib/Gzz/Text/Utils.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 8
unit module Gzz::Text::Utils:ver<0.1.23>:auth<Francis Grizzly Smit (grizzlysmit@smit.id.au)>;

=begin pod

=begin head2

Table of  Contents

=end head2

=item L<NAME|#name>
=item L<AUTHOR|#author>
=item L<VERSION|#version>
=item L<TITLE|#title>
=item L<SUBTITLE|#subtitle>
=item L<COPYRIGHT|#copyright>
=item L<Introduction|#introduction>
=item2 L<Motivations|#motivations>
=item3 L<Update|#update>
=item L<Exceptions|#exceptions>
=item2 L<BadArg|#badarg>
=item2 L<ArgParityMissMatch|#argparitymissmatch>
=item2 L<FormatSpecError|#formatspecerror>
=item L<C<UnhighlightBase> & C<UnhighlightBaseActions> and C<Unhighlight> & C<UnhighlightActions>|#unhighlightbase--unhighlightbaseactions-and-unhighlight--unhighlightactions>
=item L<The Functions Provided|#the-functions-provided>

=begin item2
L<Here are 4 functions provided  to B<C<centre>>, B<C<left>> and B<C<right>> justify text even when it is ANSI formatted|#here-are-4-functions-provided-to-centre-left-and-right-justify-text-even-when-it-is-ansi-formatted>
=end item2

=item3 L<centre(…)|#centre>
=item3 L<left(…)|#left>
=item3 L<right(…)|#right>
=item4 L<crop-field(…)|#crop-field>

=item2 L<Sprintf|#sprintf>
=item2 L<Printf|#printf>


=item2 L<MultiT|#multit>

=item2 L<menu(…)|#menu>
=item2 L<input-menu(…)|#input-menu>
=item2 L<dropdown(…)|#dropdown>
=item2 L<lead-dots(…)|#lead-dots>
=item2 L<trailing-dots(…)|#trailing-dots>
=item2 L<dots(…)|#dots>

=NAME Gzz::Text::Utils 
=AUTHOR Francis Grizzly Smit (grizzly@smit.id.au)
=VERSION v0.1.23
=TITLE Gzz::Text::Utils
=SUBTITLE A Raku module to provide text formatting services to Raku programs.

=COPYRIGHT
LGPL V3.0+ L<LICENSE|https://github.com/grizzlysmit/Gzz-Text-Utils/blob/main/LICENSE>

=head1 Introduction

A Raku module to provide text formatting services to Raku programs.

Including a sprintf front-end Sprintf that copes better with Ansi highlighted
text and implements B<C<%U>> and does octal as B<C<0o123>> or B<C<0O123>> if
you choose B<C<%O>> as I hate ambiguity like B<C<0123>> is it an int with
leading zeros or an octal number.
Also there is B<C<%N>> for a new line and B<C<%T>> for a tab helpful when
you want to use single quotes to stop the B«<num> C«$»» specs needing back slashes.

And a B<C<printf>> alike B<C<Printf>>.

Also it does centring and there is a B<C<max-width>> field in the B<C<%>> spec i.e. B<C<%*.*.*E>>, 
and more.

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

=head2 Motivations

When you embed formatting information into your text such as B<bold>, I<italics>, etc ... and B<colours>
standard text formatting will not work e.g. printf, sprintf etc also those functions don't do centring.

Another important thing to note is that even these functions will fail if you include such formatting
in the B<text> field unless you supply a copy of the text with out the formatting characters in it 
in the B<:ref> field i.e. B<C<left($formatted-text, $width, :ref($unformatted-text))>> or 
B<C<text($formatted-text, $width, :$ref)>> if the reference text is in a variable called B<C<$ref>>
or you can write it as B«C«left($formatted-text, $width, ref => $unformatted-text)»»

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

=head3 Update

Fixed the proto type of B<C<left>> etc is now 

=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

Where B«C«sub strip-ansi(Str:D $text --> Str:D) is export»» is my new function for striping out ANSI escape sequences so we don't need to supply 
B<C<:$ref>> unless it contains codes that B«C«sub strip-ansi(Str:D $text --> Str:D) is export»» cannot strip out, if so I would like to know so
I can update it to cope with these new codes.

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

=end pod

INIT my $debug = False;
####################################
#                                  #
#  To turn On or Off debugging     #
#  Comment or Uncomment this       #
#  following line.                 #
#                                  #
####################################
#INIT $debug = True; use Grammar::Debugger;

#use Grammar::Tracer;
INIT "Grammar::Debugger is on".say if $debug;

use Terminal::ANSI::OO :t;
use Term::termios;
use Terminal::Width;
use Terminal::WCWidth;
use Gzz::Prompt;

my @signal; # stuff to run on a interupt/signal #

=begin pod

=head1 Exceptions

=head2 BadArg

 
=begin code :lang<raku>
 
class BadArg is Exception is export
 
=end code
 
BadArg is a exception type that Sprintf will throw in case of badly specified arguments.

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

=end pod

class BadArg is Exception is export {
    has Str:D $.msg = 'Error: bad argument found';
    method message( --> Str:D) {
        $!msg;
    }
}

=begin pod

=head2 ArgParityMissMatch

=begin code :lang<raku>

class ArgParityMissMatch is Exception is export

=end code

ArgParityMissMatch is an exception class that Sprintf throws if the number of arguments
does not match what the number the format string says there should be.

B<NB: if you use I<C<num$>> argument specs these will not count as they grab from the
args add hoc, I<C<*>> width and precision spec however do count as they consume argument.>

=end pod

class ArgParityMissMatch is Exception is export {
    has Str:D $.msg = 'Error: argument paraity error found';
    method message( --> Str:D) {
        $!msg;
    }
}

=begin pod

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

=head2 FormatSpecError

=begin code :lang<raku>

class FormatSpecError is Exception is export

=end code

FormatSpecError is an exception class that Format (used by Sprintf) throws if there is an
error in the Format specification (i.e. B<C<%n>> instead of B<C<%N>> as B<C<%n>> is already
taken, the same with using B<C<%t>> instead of B<C<%T>>).

Or anything else wrong with the Format specifier.

B<NB: I<C<%N>> introduces a I<C<\n>> character and I<C<%T>> a tab (i.e. I<C<\t>>).>

=end pod

class FormatSpecError is Exception is export {
    has Str:D $.msg = 'Error: argument paraity error found';
    method message( --> Str:D) {
        $!msg;
    }
}
#`««
if $*RAKU.compiler.name ne 'rakudo' {
    sub dd(*@args) is export {
        say @args».raku.join("\n");
    }
}
#»»

=begin pod

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

=head1 Format and FormatActions

Format & FormatActions are a grammar and Actions pair that parse out the B<%> spec and normal text chunks of a format string.

For use by Sprintf a sprintf alternative that copes with ANSI highlighted text.

=end pod

grammar FormatBase {
    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 »
                                  || 't' #`« not implemented and will not be »
                              ]
                            }
    token fmt-spec          { [ <false-flags>? <dollar-directive> '$' ]? <flags>?  <width>? 
                             [ [ '.' <precision>? || ',' <false-percn>? ] [ '.' <max-width> || ',' <false-max> ]? ]? <modifier>? <spec-char> }
    token false-flags       { [ <false-flag>+ <?before \d+ '$' > ] } #`«« make sure that we don't see flags
                                                                          before the <dollar-directive> '$' »»
    token false-flag        { [ '+' || '^' || '-' || '#' || 'v' || '0' || ' ' || '[' <-[ <cntrl> \s \[ \] ]>+ ']' ] }
    token dollar-directive  { \d+ <?before '$'> }
    token flags             { [ [ <flag> ** {1 .. 20} ] <!before \d+ '$' > ] }
    token flag              { [ <force-sign> || <justify> || <type-prefix> || <vector> || <padding> ] }
    token force-sign        { '+' } #`« put a plus in front of positive values »
    token justify           { [     '-' #`« left justify right is the default »
                                 || '^' #`« centre justify »
                              ]
                            }
    token type-prefix       { '#' #`« ensure the leading "0" for any octal, prefix non-zero hexadecimal with "0x"
                                              or "0X", prefix non-zero binary with "0b" or "0B" »
                            }
    token vector            { 'v' #`« vector flag (used only with d directive) » }
    token padding           { [ <old-padding> || '[' <arbitrary-padding> ']' ] }
    token old-padding       { [ '0' || ' ' ] }
    token arbitrary-padding { [ <-[ <cntrl> \s \[ \] ]>+ || ' '+ ] }
    token width             { [ '*' [ <width-dollar> '$' ]? || <width-int> ] }
    token width-dollar      { \d+ <?before '$'> }
    token width-int         { \d+ }
    token precision         { [ '*' [ <prec-dollar> '$' ]?  || <prec-int>  ] }
    token prec-dollar       { \d+ <?before '$'> }
    token prec-int          { \d+ }
    token false-percn       { [ '*' [ \d+ '$' ]? || \d+ ] }
    token false-max         { [ '*' [ \d+ '$' ]? || \d+ ] }
    token max-width         { [ '*' [ <max-dollar> '$' ]?  || <max-int>  ] }
    token max-dollar        { \d+ <?before '$'> }
    token max-int           { \d+ }
    token modifier          { [           #`« (Note: None of the following have been implemented.) »
                                     'hh' #`« interpret integer as a type "char" or "unsigned char" »
                                  || 'h'  #`« interpret integer as a type "short" or "unsigned short" »
                                  || 'j'  #`« interpret integer as a type "intmax_t", only with a C99 compiler (unportable) »
                                  || 'l'  #`« interpret integer as a type "long" or "unsigned long" »
                                  || 'll' #`« interpret integer as a type "long long", "unsigned long long", or "quad" (typically 64-bit integers) »
                                  || 'q'  #`« interpret integer as a type "long long", "unsigned long long", or "quad" (typically 64-bit integers) »
                                  || 'L'  #`« interpret integer as a type "long long", "unsigned long long", or "quad" (typically 64-bit integers) »
                                  || 't'  #`« interpret integer as a type "ptrdiff_t" »
                                  || 'z'  #`« interpret integer as a type "size_t" »
                             ]
                            }
    token spec-char         { [      'c' #`« a character with the given codepoint »
                                  || 's' #`« a string »
                                  || 'd' #`« a signed integer, in decimal »
                                  || 'u' #`« an unsigned integer, in decimal »
                                  || 'o' #`« an unsigned integer, in octal »
                                  || 'x' #`« an unsigned integer, in hexadecimal »
                                  || 'e' #`« a floating-point number, in scientific notation »
                                  || 'f' #`« a floating-point number, in fixed decimal notation »
                                  || 'g' #`« a floating-point number, in %e or %f notation »
                                  || 'X' #`« like x, but using uppercase letters »
                                  || 'E' #`« like e, but using an uppercase "E" »
                                  || 'G' #`« like g, but with an uppercase "E" (if applicable) »
                                  || 'b' #`« an unsigned integer, in binary »
                                  || 'B' #`« an unsigned integer, in binary »
                                         #`« Compatibility: »
                                  || 'i' #`« a synonym for %d »
                                  || 'D' #`« a synonym for %ld »
                                  || 'U' #`« a synonym for %lu »
                                  || 'O' #`« a synonym for %lo »
                                  || 'F' #`« a synonym for %f »
                              ]
                            }
} # grammar FormatBase #

role FormatBaseActions {
    method false-flag($/) {
        my $false-flag = ~$/;
        dd $false-flag if $debug;
        make $false-flag;
    }
    method false-flags($/) {
        my $false-flags = $/<false-flag>».made;
        dd $false-flags if $debug;
        FormatSpecError.new(:msg("Error flags belong after <number> '\$' spec not before.")).throw;
        make $false-flags;
    }
    method dollar-directive($/) {
        my Int $dollar-directive = +$/ - 1;
        FormatSpecError.new(:msg("bad \$ spec for arg: cannot be less than 1 ")).throw if $dollar-directive < 0;
        dd $dollar-directive if $debug;
        make $dollar-directive;
    }
    method flags($/) {
        my @flags = $/<flag>».made;
        dd @flags if $debug;
        make @flags;
    }
    #token force-sign        { '+' } #`« put a plus in front of positive values »
    method force-sign($/) {
        my $force-sign = ~$/;
        dd $force-sign if $debug;
        make $force-sign;
    }
    #token justify           { [     '-' #`« left justify right is the default »
    #                             || '^' #`« centre justify »
    #                          ]
    #                        }
    method justify($/) {
        my $justify = ~$/;
        dd $justify if $debug;
        make $justify;
    }
    #token type-prefix       { '#' #`« ensure the leading "0" for any octal, prefix non-zero hexadecimal with "0x"
    #                                          or "0X", prefix non-zero binary with "0b" or "0B" »
    #                        }
    method type-prefix($/) {
        my $type-prefix = ~$/;
        dd $type-prefix if $debug;
        make $type-prefix;
    }
    #token vector            { 'v' #`« vector flag (used only with d directive) » }
    method vector($/) {
        my $vector = ~$/;
        dd $vector if $debug;
        make $vector;
    }
    #token old-padding       { [ '0' || ' ' ] }
    method old-padding($/) {
        my $old-padding = ~$/;
        dd $old-padding if $debug;
        make $old-padding;
    }
    #token arbitrary-padding { [ <-[ <cntrl> \s \[ \] ]>+ || ' ' ] }
    method arbitrary-padding($/) {
        my $arbitrary-padding = ~$/;
        if $arbitrary-padding.codes != 1 {
            my Str:D $msg = "Error: should only contain one codepoint/character you supplied {$arbitrary-padding.codes}: [$arbitrary-padding]";
            FormatSpecError.new(:$msg).throw;
        }
        dd $arbitrary-padding if $debug;
        make $arbitrary-padding;
    }
    #token padding           { [ <old-padding> || '[' <arbitrary-padding> ']' ] }
    method padding($/) {
        my $padding;
        if $/<old-padding> {
            $padding = $/<old-padding>.made;
        } elsif $/<arbitrary-padding> {
            $padding = $/<arbitrary-padding>.made;
        }
        dd $padding if $debug;
        make $padding;
    }
    #token flag              { [ <force-sign> || <justify> || <type-prefix> || <vector> || <padding> ] }
    method flag($/) {
        my %flag;
        if $/<force-sign> {
            %flag = kind => 'force-sign', val => $/<force-sign>.made;
        } elsif $/<justify> {
            %flag = kind => 'justify', val => $/<justify>.made;
        } elsif $/<type-prefix> {
            %flag = kind => 'type-prefix', val => $/<type-prefix>.made;
        } elsif $/<vector> {
            %flag = kind => 'vector', val => $/<vector>.made;
        } elsif $/<padding> {
            %flag = kind => 'padding', val => $/<padding>.made;
        }
        dd %flag if $debug;
        make %flag;
    }
    #token width            { [ '*' [ <width-dollar> '$' ]? || <width-int> ] }
    #token width-dollar     { \d+ }
    #token width-int        { \d+ }
    method width-dollar($/) {
        my Int:D $width-dollar = +$/ - 1;
        FormatSpecError.new(:msg('bad $ spec for width: cannot be less than 1 ')).throw if $width-dollar < 0;
        dd $width-dollar if $debug;
        make $width-dollar;
    }
    method width-int($/) {
        my Int:D $width-int = +$/;
        dd $width-int if $debug;
        make $width-int;
    }
    method width($/) {
        my %width = kind => 'star', val => 0;
        if $/<width-dollar> {
            %width = kind => 'dollar', val => $/<width-dollar>.made;
        } elsif $/<width-int> {
            %width = kind => 'int', val => $/<width-int>.made;
        }
        dd %width if $debug;
        make %width;
    }
    #token prec-dollar      { \d+ }
    method prec-dollar($/) {
        my Int:D $prec-dollar = +$/ - 1;
        FormatSpecError.new(:msg('bad $ spec for precision: cannot be less than 1 ')).throw if $prec-dollar < 0;
        dd $prec-dollar if $debug;
        make $prec-dollar;
    }
    #token prec-int         { \d+ }
    method prec-int($/) {
        my Int:D $prec-int = +$/;
        dd $prec-int if $debug;
        make $prec-int;
    }
    #token precision        { [ '*' [ <prec-dollar> '$' ]?  || <prec-int>  ] }
    method precision($/) {
        my %precision = kind => 'star', val => 0;
        if $/<prec-dollar> {
            %precision = kind => 'dollar', val => $/<prec-dollar>.made;
        } elsif $/<prec-int> {
            %precision = kind => 'int', val => $/<prec-int>.made;
        }
        dd %precision if $debug;
        make %precision;
    }
    #token false-percn       { [ '*' [ \d+ '$' ]? || \d+ ] }
    method false-percn($/) {
        my Str:D $false-percn = ~$/;
        FormatSpecError.new(:msg("Error: found comma (',') instead of dot ('.') before precision in '%' spec.")).throw;
        dd $false-percn if $debug;
        make $false-percn;
    }
    #token false-max         { [ '*' [ \d+ '$' ]? || \d+ ] }
    method false-max($/) {
        my Str:D $false-max = ~$/;
        FormatSpecError.new(:msg("Error: found comma (',') instead of dot ('.') before max-width in '%' spec.")).throw;
        dd $false-max if $debug;
        make $false-max;
    }
    #token max-dollar        { \d+ <?before '$'> }
    method max-dollar($/) {
        my Int:D $max-dollar = +$/ - 1;
        FormatSpecError.new(:msg('bad $ spec for max-width: cannot be less than 1 ')).throw if $max-dollar < 0;
        dd $max-dollar if $debug;
        make $max-dollar;
    }
    #token max-int           { \d+ }
    method max-int($/) {
        my Int:D $max-int = +$/;
        dd $max-int if $debug;
        make $max-int;
    }
    #token max-width         { [ '*' [ <max-dollar> '$' ]?  || <max-int>  ] }
    method max-width($/) {
        my %max-width = kind => 'star', val => 0;
        if $/<max-dollar> {
            %max-width = kind => 'dollar', val => $/<max-dollar>.made;
        } elsif $/<max-int> {
            %max-width = kind => 'int', val => $/<max-int>.made;
        }
        dd %max-width if $debug;
        make %max-width;
    }
    method modifier($/) {
        my Str $modifier = ~$/;
        dd $modifier if $debug;
        make $modifier;
    }
    method spec-char($/) {
        my Str:D $spec-char = ~$/;
        dd $spec-char if $debug;
        make $spec-char;
    }
    #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 »
    #                              || 't' #`« not implemented and will not be »
    #                          ]
    #                        }
    method fmt-esc($/) {
        my %fmt-esc = type => 'literal', val => ~$/;
        %fmt-esc«val» = "\n" if %fmt-esc«val» eq 'N'; # %N gives us an newline saves on needing double quotes #
        %fmt-esc«val» = "\t" if %fmt-esc«val» eq 'T'; # %T gives us an tab saves on needing double quotes #
        dd %fmt-esc if $debug;
        FormatSpecError.new(:msg("%n not implemented and will not be; did you mean %N.")).throw if %fmt-esc«val» eq 'n'; # not implemented and will not be. #
        FormatSpecError.new(:msg("%t not implemented and will not be; did you mean %T.")).throw if %fmt-esc«val» eq 't'; # not implemented and will not be. #
        make %fmt-esc;
    }
    #token fmt-spec          { [ <dollar-directive> '$' ]? <flags>?  <width>? [ '.' <precision> [ '.' <max-width> ]? ]? <modifier>? <spec-char> }
    method fmt-spec($/) {
        my %fmt-spec = type => 'fmt-spec', dollar-directive => -1, flags => [],
                                  width => { kind => 'empty', val => 0, },
                                    precision => { kind => 'empty', val => 0, },
                                       max-width => { kind => 'empty', val => 0},
                                         modifier => '', spec-char => $/<spec-char>.made;
        if $/<dollar-directive> {
            %fmt-spec«dollar-directive» = $/<dollar-directive>.made;
        }
        if $/<flags> {
            %fmt-spec«flags» = $/<flags>.made;
        }
        if $/<width> {
            %fmt-spec«width» = $/<width>.made;
        }
        if $/<precision> {
            %fmt-spec«precision» = $/<precision>.made;
        }
        if $/<max-width> {
            %fmt-spec«max-width» = $/<max-width>.made;
        }
        if $/<modifier> {
            %fmt-spec«modifier» = $/<modifier>.made;
        }
        dd %fmt-spec if $debug;
        make %fmt-spec;
    }
    #token format-spec      { [ <fmt-esc> || <fmt-spec> ] }
    method format-spec($/) {
        my %format-spec;
        if $/<fmt-esc> {
            %format-spec = $/<fmt-esc>.made;
        } elsif $/<fmt-spec> {
            %format-spec = $/<fmt-spec>.made;
        }
        dd %format-spec if $debug;
        make %format-spec;
    }
    #token chunk            { <-[%]>+ }
    method chunk($/) {
        my %chunk = type => 'literal', val => ~$/;
        dd %chunk if $debug;
        make %chunk;
    }
    #token chunks           { [ <chunk> || '%' <format-spec> ] }
    method chunks($/) {
        my %chunks;
        if $/<chunk> {
            %chunks = $<chunk>.made;
        } elsif $/<format-spec> {
            %chunks = $/<format-spec>.made;
        }
        dd %chunks if $debug;
        make %chunks;
    }
    #token format           { <chunks>+ }
    method format($/) {
        my @format = $/<chunks>».made;
        dd @format if $debug;
        make @format;
    }
} # role FormatBaseActions #

grammar Format is FormatBase is export {
    token TOP      { ^ <format> $ }
}

class FormatActions does FormatBaseActions is export {
    method TOP($made) {
        my @top = $made<format>.made;
        dd @top if $debug;
        $made.make: @top;
    }
} # class FormatActions does FormatBaseActions is export # 

=begin pod

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

=head1 C<UnhighlightBase> & C<UnhighlightBaseActions> and C<Unhighlight> & C<UnhighlightActions>

B<C<UnhighlightBase>> & B<C<UnhighlightBaseActions>> are a grammar & role pair that does the work required to 
to parse apart ansi highlighted text into ANSI highlighted and plain text. 

B<C<Unhighlight>> & B<C<UnhighlightActions>> are a grammar & class pair which provide a simple TOP for applying
an application of B<C<UnhighlightBase>> & B<C<UnhighlightBaseActions>>  for use by
B<C<sub strip-ansi(Str:D $text --> Str:D) is export>> to strip out the plain text from a ANSI formatted string

=end pod

grammar UnhighlightBase is export {
    token text           { ^ [  <empty> <!before . > || <block> ] $ } #`««« the <!before .+ > is vital here as
                                                                             otherwise empty will match before
                                                                             anything, and the whole match will
                                                                             fail, otherwise one could put
                                                                             <block> before <empty> but then
                                                                             we will need to do more work whereas
                                                                             <empty> <!before . > succeeds or
                                                                             fails much more quickly »»»
    token block          { <chunks>+ }
    token empty          { '' }
    token chunks         { [ <chunk> || <ansi> ] }
    token chunk          { <-[ \x[1B] ]>+ }
    token ansi           { [ <clear-screen> || <home> || <move-to> || <reset-scroll-region>
                             || <set-scroll-region> || <scroll-down> || <scroll-up>
                             || <save-screen> || <hide-cursor> || <restore-screen>
                             || <show-cursor> || <cursor-up> || <cursor-down>
                             || <cursor-right> || <cursor-left> | <cursor-next-line>
                             || <cursor-prev-line> || <print-at> || <set-fg-color>
                             || <set-fg-rgb-color> || <set-bg-color> || <set-bg-rgb-color>
                             || <set-bg-default> || <save-cursor> || <restore-cursor>
                             || <start-of-line> || <erase-to-end-of-line> || <normal-video> 
                             || <bold> || <faint> || <italic> || <underline> || <blink>
                             || <reverse-video> || <strike> || <alt-font>
                           ]
                         }
    token clear-screen         { "\e[H\e[J" }
    token home                 { "\e[H" }
    token move-to              { "\e[" \d+ ';' \d+ 'H' }
    token reset-scroll-region  { "\e[r" }
    token set-scroll-region    { "\e[" \d+ ';' \d+ 'r' }
    token scroll-down          { "\e[M" }
    token scroll-up            { "\e[D" }
    token hide-cursor          { "\e[?25l" }
    token save-screen          { "\e[?1049h" }
    token restore-screen       { "\e[?1049l" }
    token show-cursor          { "\e[25h" }
    token cursor-up            { "\e[" \d+ 'A' }
    token cursor-down          { "\e[" \d+ 'B' }
    token cursor-right         { "\e[" \d+ 'C' }
    token cursor-left          { "\e[" \d+ 'D' }
    token cursor-next-line     { "\e[" \d+ 'E' }
    token cursor-prev-line     { "\e[" \d+ 'F' }
    token print-at             { "\e[" \d+ ';' \d+ 'H' }
    token set-fg-color         { "\e[38;5;" \d+ 'm' }
    token set-fg-rgb-color     { "\e[38;2;" \d+ ';' \d+ ';' \d+ 'm' }
    token set-bg-color         { "\e[48;5;" \d+ 'm' }
    token set-bg-rgb-color     { "\e[48;2;" \d+ ';' \d+ ';' \d+ 'm' }
    token set-bg-default       { "\e[49;m" }
    token save-cursor          { "\e[s" }
    token restore-cursor       { "\e[u" }
    token start-of-line        { "\e[\r" }
    token erase-to-end-of-line { "\e[2J" }
    token normal-video         { "\e[0m" }
    token bold                 { "\e[1m" }
    token faint                { "\e[2m" }
    token italic               { "\e[3m" }
    token underline            { "\e[4m" }
    token blink                { "\e[5m" }
    token reverse-video        { "\e[7m" }
    token strike               { "\e[9m" }
    token alt-font             { [ "\e[11m" || "\e[12m" || "\e[13m" || "\e[14m" || "\e[15m"
                                   "\e[16m" || "\e[17m" || "\e[18m" || "\e[19m" 
                                 ] }
} # grammar UnhighlightBase is export #

role UnhighlightBaseActions is export {
    method clear-screen($/) {
        my %clear-screen = type => 'ansi', sub-type => 'clear-screen', val => ~$/;
        dd %clear-screen if $debug;
        make %clear-screen;
    }
    method home($/) {
        my %home = type => 'ansi', sub-type => 'home', val => ~$/;
        dd %home if $debug;
        make %home;
    }
    method move-to($/) {
        my %move-to = type => 'ansi', sub-type => 'move-to', val => ~$/;
        dd %move-to if $debug;
        make %move-to;
    }
    method reset-scroll-region($/) {
        my %reset-scroll-region = type => 'ansi', sub-type => 'reset-scroll-region', val => ~$/;
        dd %reset-scroll-region if $debug;
        make %reset-scroll-region;
    }
    method set-scroll-region($/) {
        my %set-scroll-region = type => 'ansi', sub-type => 'set-scroll-region', val => ~$/;
        dd %set-scroll-region if $debug;
        make %set-scroll-region;
    }
    method scroll-down($/) {
        my %scroll-down = type => 'ansi', sub-type => 'scroll-down', val => ~$/;
        dd %scroll-down if $debug;
        make %scroll-down;
    }
    method scroll-up($/) {
        my %scroll-up = type => 'ansi', sub-type => 'scroll-up', val => ~$/;
        dd %scroll-up if $debug;
        make %scroll-up;
    }
    method hide-cursor($/) {
        my %hide-cursor = type => 'ansi', sub-type => 'hide-cursor', val => ~$/;
        dd %hide-cursor if $debug;
        make %hide-cursor;
    }
    method save-screen($/) {
        my %save-screen = type => 'ansi', sub-type => 'save-screen', val => ~$/;
        dd %save-screen if $debug;
        make %save-screen;
    }
    method restore-screen($/) {
        my %restore-screen = type => 'ansi', sub-type => 'restore-screen', val => ~$/;
        dd %restore-screen if $debug;
        make %restore-screen;
    }
    method show-cursor($/) {
        my %show-cursor = type => 'ansi', sub-type => 'show-cursor', val => ~$/;
        dd %show-cursor if $debug;
        make %show-cursor;
    }
    method cursor-up($/) {
        my %cursor-up = type => 'ansi', sub-type => 'cursor-up', val => ~$/;
        dd %cursor-up if $debug;
        make %cursor-up;
    }
    method cursor-down($/) {
        my %cursor-down = type => 'ansi', sub-type => 'cursor-down', val => ~$/;
        dd %cursor-down if $debug;
        make %cursor-down;
    }
    method cursor-right($/) {
        my %cursor-right = type => 'ansi', sub-type => 'cursor-right', val => ~$/;
        dd %cursor-right if $debug;
        make %cursor-right;
    }
    method cursor-left($/) {
        my %cursor-left = type => 'ansi', sub-type => 'cursor-left', val => ~$/;
        dd