### ----------------------------------------------------
### -- Terminal::ReadKey
### -- Licenses: Artistic-2.0
### -- Authors: thundergnat
### -- File: lib/Terminal/ReadKey.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 2
 59, 52, 126).decode => ｢Alt Shift F7｣,
    Buf.new(27, 91, 49, 57, 59, 53, 126).decode => ｢Ctrl F8｣,
    Buf.new(27, 91, 49, 57, 59, 54, 126).decode => ｢Ctrl Shift F7｣,
    Buf.new(27, 91, 49, 57, 59, 56, 126).decode => ｢Ctrl Alt Shift F7｣,
    Buf.new(27, 91, 50, 48, 59, 50, 126).decode => ｢Shift F9｣,
    Buf.new(27, 91, 50, 48, 59, 51, 126).decode => ｢Alt F9｣,
    Buf.new(27, 91, 50, 48, 59, 52, 126).decode => ｢Alt Shift F9｣,
    Buf.new(27, 91, 50, 48, 59, 53, 126).decode => ｢Ctrl F9｣,
    Buf.new(27, 91, 50, 48, 59, 54, 126).decode => ｢Ctrl Shift F9｣,
    Buf.new(27, 91, 50, 48, 59, 56, 126).decode => ｢Ctrl Alt Shift F9｣,
    Buf.new(27, 91, 50, 49, 59, 50, 126).decode => ｢Shift F10｣,
    Buf.new(27, 91, 50, 49, 59, 51, 126).decode => ｢Alt F10｣,
    Buf.new(27, 91, 50, 49, 59, 52, 126).decode => ｢Alt Shift F10｣,
    Buf.new(27, 91, 50, 49, 59, 53, 126).decode => ｢Ctrl F10｣,
    Buf.new(27, 91, 50, 49, 59, 54, 126).decode => ｢Ctrl Shift F10｣,
    Buf.new(27, 91, 50, 49, 59, 56, 126).decode => ｢Ctrl Alt Shift F10｣,
    Buf.new(27, 91, 50, 51, 59, 50, 126).decode => ｢Shift F11｣,
    Buf.new(27, 91, 50, 51, 59, 51, 126).decode => ｢Alt F11｣,
    Buf.new(27, 91, 50, 51, 59, 52, 126).decode => ｢Alt Shift F11｣,
    Buf.new(27, 91, 50, 51, 59, 53, 126).decode => ｢Ctrl F11｣,
    Buf.new(27, 91, 50, 51, 59, 54, 126).decode => ｢Ctrl Shift F11｣,
    Buf.new(27, 91, 50, 51, 59, 56, 126).decode => ｢Ctrl Alt Shift F11｣,
    Buf.new(27, 91, 50, 52, 59, 50, 126).decode => ｢Shift F12｣,
    Buf.new(27, 91, 50, 52, 59, 51, 126).decode => ｢Alt F12｣,
    Buf.new(27, 91, 50, 52, 59, 52, 126).decode => ｢Alt Shift F12｣,
    Buf.new(27, 91, 50, 52, 59, 53, 126).decode => ｢Ctrl F12｣,
    Buf.new(27, 91, 50, 52, 59, 54, 126).decode => ｢Ctrl Shift F12｣,
    Buf.new(27, 91, 50, 52, 59, 56, 126).decode => ｢Ctrl Alt Shift F12｣,
};

sub cooked ($char, :$layout = 'US') is export(:cooked) {
    %keyboard{$layout}{$char} ?? %keyboard{$layout}{$char} !!
    $char.substr(0,1) eq Buf.new(27).decode ?? $char.ords !!
    $char
}

sub with-termios(Callable:D $fn, Bool:D :$echo = True) is export(:_testing) {
    my $original-flags := $termios.new(:fd($*IN.native-descriptor)).getattr;
    my $flags := $termios.new(:fd($*IN.native-descriptor)).getattr;

    $flags.unset_lflags('ICANON');
    $flags.unset_lflags('ECHO') unless $echo;
    $flags.setattr(:NOW);

    my $result := $fn();
    $original-flags.setattr(:NOW);
    $result;
}

sub key-pressed (
  Bool:D :$echo = False,
  Bool:D :$raw = False,
  :$layout = 'US'
  --> Supply
  ) is export {
    my Supplier $supplier .= new;
    my $done = False;
    my $supply = $supplier.Supply.on-close: { $done = True };

    start {
        with-termios(
            sub {
                until $done {
                    my $char = $*IN.read(10).decode;
                    $supplier.emit: $raw ?? $char !! cooked($char, :$layout);
                }
            },
            :$echo
        )
    }
    $supply
}

=begin pod

=head1 NAME

Terminal::ReadKey - Read non-blocking key presses from the keyboard buffer.

=head1 SYNOPSIS

=begin code :lang<raku>

use Terminal::ReadKey;

react { whenever key-pressed() { .say } }

=end code

=head1 DESCRIPTION

Terminal::ReadKey makes it easy to get non-blocking key presses from the
keyboard buffer, even multibyte, combination key presses.

=head1 USAGE

Terminal::ReadKey has a few options you can set to modify its behavior.

Defaults to cooked mode, where it returns a string containing the named key
press. If you want the byte values instead, pass in the parameter :raw set to a
truthy value. Raw mode returns a sequence of bytes decoded to an ASCII string.
For printable characters it is effectively the same as cooked mode. For
non-printables, (function keys, navigation keys, Alt, or Ctrl keys) it won't
look like much.

When in cooked mode, if there is an unrecognized keypress, returns the byte
string as a stringified list of ordinals.

By default, does not echo each keypress to STDOUT. If you would prefer it to,
pass in the parameter :echo set to a truthy value.

=begin code :lang<raku>

use Terminal::ReadKey;

# cooked mode
react { whenever key-pressed() { .say } }

=end code

or

=begin code :lang<raku>

use Terminal::ReadKey;

# raw mode
react { whenever key-pressed( :raw ) { .ords.say } }

=end code

Also, selectively exports the cooked() routine if you want to convert raw output
to cooked manually.

=begin code :lang<raku>

use Terminal::ReadKey :cooked;

my $fh = open( :a, 'key-cap.txt' ) or die $_;
react { whenever key-pressed( :raw, :echo ) { $fh.print( .&cooked ); $fh.flush } }

=end code

=head1 BUGS

Since Terminal::ReadKey interacts with the keyboard buffer rather than reading
the keyboard directly, it has no way to differentiate different keypress
sequences that result in the same keypress code(s) in the buffer. For instance,
if it reads a "Ctrl PgUp", it has no way to tell _which_ Ctrl key or _which_
PgUp key may have been pressed, only that there _was_ one of each pressed. Same
with Shift, Enter, or any other key combination that may be entered in multiple
ways.

That also means that it can be affected by the OS / window managers typing
repeat setting. It doesn't see, and is not responding to the actual key press,
rather the presence of key codes in the keyboard buffer.

All of the Ctrl plus an alphabetic key sequences return uppercase alphabetics,
regardless of the case of the key pressed. Not really a bug, just the reality of
the situation, something to be aware of. 'Ctrl a' and 'Ctrl A' keypresses both
return the same key codes so both evaluate to 'Ctrl A'. There isn't any way to
differentiate.

There are a few key combinations that I haven't yet been able to capture because
something else grabs the key code before it can be processed.

 Notably missing (or at least, unverified):

=item Alt F1
=item Alt F2
=item Alt F3
=item Alt Tab
=item Ctrl C
=item Ctrl Z
=item Ctrl Insert
=item Ctrl Shift |
=item Shift PgUp
=item Shift PgDn
=item Shift Home
=item Shift End
=item Shift Insert

The key combination "Shift Insert" stuffs the keyboard buffer with whatever is
in the cut/paste buffer rather than the actual key codes, so doesn't reliably
return a repeatable keypress sequence (if the cut/paste buffer contents
changes).


This was written and tested on a US English keyboard. Characters on non-US
keyboards may not have correct "cooked" mode support.


=head1 AUTHOR

Based on Term::ReadKey by jkramer - github.com/jkramer/p6-Term-ReadKey

Modified to recognize multibyte (non-printable) key presses and support "cooked"
mode by Stephen Schulze - aka thundergnat <thundergnat@gmail.com>

=head1 COPYRIGHT AND LICENSE

Copyright 2022 Stephen Schulze

This library is free software; you can redistribute it and/or modify it under
the Artistic License 2.0.

=end pod


