Key Capture

The Fortran language standard does not offer intrinsic procedures to re-configure the terminal in order to read single key-strokes (blocking or non-blocking). For POSIX-style tty control, some sort of interface bindings are required:

For non-blocking keyboard input, one may interface the C procedures listed on Rosetta Code, or change the examples accordingly.

stty

On Unix, the command-line tool stty(1) is used to configure the terminal device interface. The cbreak mode lets us read single characters from terminal. We could either write interfaces to ioctl(2) to modify the device parameters, or, just execute stty through the Fortran intrinsic routine execute_command_line().

For both cases, the C function getchar(3) in libc returns the next input character from the input stream. We simply define an ISO C binding interface to the C function in order to read a single character in Fortran.

! key.f90
program main
    use, intrinsic :: iso_c_binding, only: c_int
    implicit none

    interface
        ! int getchar(void)
        function c_getchar() bind(c, name='getchar')
            import :: c_int
            implicit none
            integer(kind=c_int) :: c_getchar
        end function c_getchar
    end interface

    integer :: ch

    ! Enable cbreak mode.
    call execute_command_line('stty -echo cbreak </dev/tty >/dev/tty 2>&1')

    print '("Press <q> to quit.")'

    do
        ch = c_getchar()
        print '("Key pressed: ", i0)', ch
        if (ch == iachar('q')) exit
    end do

    ! Disable cbreak mode.
    call execute_command_line('stty echo -cbreak </dev/tty >/dev/tty 2>&1')
end program main

Compile and run the example program with:

$ gfortran12 -o key key.f90
$ ./key
Press <q> to quit.
Key pressed: 70

termios

The termios(4) API lets us control terminal I/O on Unix. We have to define a C routine setmode() to disable echo and line editing mode, and another C routine nextchar() to read a single input character:

/* term.c */
#include <stdio.h>
#include <termios.h>

void setmode(int *mode)
{
    static struct termios termattr, saveattr;

    if (*mode != 0)
    {
        tcgetattr(0, &termattr);

        saveattr = termattr;
        termattr.c_lflag &= ~(ICANON | ECHO);
        termattr.c_cc[VMIN] = 1;
        termattr.c_cc[VTIME] = 0;

        tcsetattr(0, TCSADRAIN, &termattr);
    }
    else
    {
        tcsetattr(0, TCSADRAIN, &saveattr);
    }
}

void nextchar(int *nextch)
{
    *nextch = getchar();
}

In order to access these routines from Fortran, ISO C binding interfaces have to be implemented. The following example prints the code of each pressed key.

! key.f90
program main
    use, intrinsic :: iso_c_binding
    implicit none

    interface
        subroutine c_setmode(mode) bind(c, name='setmode')
            import :: c_int
            implicit none
            integer(kind=c_int) :: mode
        end subroutine c_set_mode

        subroutine c_nextchar(nextch) bind(c, name='nextchar')
            import :: c_int
            implicit none
            integer(kind=c_int) :: nextch
        end subroutine c_next_char
    end interface

    integer :: key

    ! Enable single key capture.
    call c_setmode(1)

    print '("Press <q> to quit.")'

    do
        ! Read single character.
        call c_nextchar(key)
        print '("Key pressed: ", i0)', key
        if (key == iachar('q')) exit
    end do

    ! Revert to default.
    call c_setmode(0)
end program main

Compile the example with GCC:

$ gcc12 -c term.c
$ gfortran12 -o key key.f90 term.o

Or, instead, using Clang/Flang:

$ clang -c term.c
$ flang -o key key.f90 term.o

The code of each pressed key will be printed to screen until the user hits q:

$ ./key
Press <q> to quit.
Key pressed: 70

fortran-unix

The fortran-unix library provides interface bindings to common POSIX routines, among them tcgetattr(3) and tcsetattr(3) which allows us to read and write terminal settings without an abstraction layer in C.

First, clone the fortran-unix repository and build the static library libfortran-unix.a:

$ git clone https://github.com/interkosmos/fortran-unix
$ cd fortran-unix/
$ make freebsd

On Linux, run instead:

$ make linux

The following example program is nearly identical to the one listed in section termios, with the C part implemented in Fortran. The terminal attributes are set through the interface bindings provides by the unix module.

! key.f90
program main
    use :: unix
    implicit none
    integer :: key

    ! Enable single key capture.
    call set_mode(1)

    print '("Press <q> to quit.")'

    do
        ! Read single character.
        key = next_char()
        print '("Key pressed: ", i0)', key
        if (key == iachar('q')) exit
    end do

    ! Revert to default.
    call set_mode(0)
contains
    integer function next_char() result(i)
        i = c_getchar()
    end function next_char

    subroutine set_mode(mode)
        integer, intent(in)   :: mode
        integer               :: rc
        type(c_termios)       :: term_attr
        type(c_termios), save :: save_attr

        if (mode /= 0) then
            rc = c_tcgetattr(0, term_attr)

            save_attr = term_attr

            term_attr%c_lflag     = iand(term_attr%c_lflag, not(ior(ICANON, ECHO)))
            term_attr%c_cc(VMIN)  = 1
            term_attr%c_cc(VTIME) = 0

            rc = c_tcsetattr(0, TCSADRAIN, term_attr)
        else
            rc = c_tcsetattr(0, TCSADRAIN,save_attr)
        end if
    end subroutine set_mode
end program main

Compile and link the program against libfortran-unix.a:

$ gfortran -o key key.f90 libfortran-unix.a

The code of each pressed key will be printed to screen until the user hits q:

$ ./key
Press <q> to quit.
Key pressed: 70

References