Joystick position

Linux only terminal based joystick testing utility. Reads events from the joystick asynchronously, allows a main processing loop if desired. This uses the main loop to check for and compensate for a resized terminal but not really anything else. Hit control-c to exit; needs one extra event from the the joystick to exit completely. Only shows the first 3 axes, no matter how many are available. Tested with an Logitech extreme joystick and an Xbox controller.

use experimental :pack;

# Joysticks generally show up in the /dev/input/ directory as js(n) where n is
# the number assigned by the OS. E.G. /dev/input/js1 . In my particular case:

my $device = '/dev/input/js0';

my $exit = 0;

my $event-stream = $device.IO.open(:bin);
my $js = $event-stream.Supply(:8size);

my %config; # joystick configuration: number of axes and buttons
my %event;  # global "joystick event"

%config<button>.push: 0;

my $callback = sub { update };

sub get-js-event ( $ev, &callback ) {
    exit if $exit;
    # 32 bit timestamp milliseconds. Allows easy checking for "double-click" button presses
    %event<timestamp> = $ev.subbuf(0, 4).reverse.unpack('N');

    # 16 bit (signed int16) value of current control
    %event<value> = (my $v = $ev.subbuf(4, 2).unpack('S')) > 32767 ?? -65536 + $v !! $v;

    # Two 8 bit integers, current event: control type, and control ID
    (%event<type>, %event<number>) = $ev.subbuf(6).unpack('CC');

    # Process the event
    if %event<type> +& 128 {       # initialing
        given %event<type> +& 3  { # enumeration of control inputs
            when 1 { %config<button>.push: %event<number> }
            when 2 { %config<axis>.push: %event<number> }
        }
    } else {
        # Optional callback subroutine to run every time a js event is received
        callback
    }
}

# read events from the joystick driver asynchronously
start react whenever $js { $js.act: { get-js-event($_, $callback) } }

# allow a short delay while driver initializes
sleep .5;

# clean up on exit
signal(SIGINT).tap: { 
    print "\e[0m", "\n" xx 50, "\e[H\e[J\e[?25hWaiting for one more joystick event...\n";
    $exit = 1;
    exit(0);
}

use Terminal::ANSIColor;

my ($rows, $cols) = qx/stty size/.words; # get the terminal size

my $xhair = '╺╋╸';
my $axis  = '█';
my @btext = %config<button>.map: { sprintf( "%2d", $_) };
my @button = @btext.map: {color('bold white on_blue ') ~ $_ ~ color('reset')};
my ($x, $y, $z) = ($rows/2).floor, ($cols/2).floor, 0;

sub update {
    given %event<type> {
        when 1 { # button event
            given %event<value> {
                when 0 { @button[%event<number>] = color('bold white on_blue ') ~ @btext[%event<number>] ~ color('reset') }
                when 1 { @button[%event<number>] = color('bold white on_green') ~ @btext[%event<number>] ~ color('reset') }
            }
        }
        when 2 { # axis events
            given %event<number> {
                when 0 { $y = ($cols / 2 + %event<value> / 32767 * $cols / 2).Int max 1 }
                when 1 { $x = ($rows / 2 + %event<value> / 32767 * $rows / 2).Int max 2 }
                when 2 { $z = (%event<value> / 32767 * 100).Int }
                default { } # only using the first 3 axes, ignore ant others
            }
            $x min= $rows - 1;
            $y min= $cols - 1;
        }
    }
    print "\e[H\e[J\e[1;1H";
    print "  ", join "  ", flat @button, "Axis 0: $x", "Axis 1: $y" , "Axis 2: $z%\n";
    my $bar = ($z / 100 * $cols / 2).floor;
    if $bar < 0 {
        print ' ' x ($bar + $cols / 2).floor, color('bold green') ~ $axis x -$bar ~ color('reset');
    } else {
        print ' ' x $cols / 2, color('bold green') ~ $axis x $bar ~ color('reset');
    }
    print "\e[{$x};{$y}H", color('bold yellow') ~ $xhair ~ color('reset');
}

print "\e[?25l"; # hide the cursor
update; # initial update

# Main loop, operates independently of the joystick event loop
loop {
    once say " Joystick has {%config<axis>.elems} axes and {%config<button>.elems} buttons";
    sleep 1;
    ($rows, $cols) = qx/stty size/.words;
}