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:
- The terminal device interface may be configured using the stty command-line utility. We can then read input with getchar(3).
- A termios abstraction layer in C may be called to alter the terminal settings from Fortran.
- The fortran-unix library provides interface bindings to directly call tcsetattr(3) from Fortran, without wrapper procedures in C.
- Alternatively, the ncurses library lets us change terminal settings as well.
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) from libc returns the next 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
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 :: ich
! Enable cbreak mode.
call execute_command_line('stty -echo cbreak </dev/tty >/dev/tty 2>&1')
print '("Press <q> to quit.")'
do
ich = c_getchar()
print '("Key pressed: ", i0)', ich
if (ich == 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:
$ gfortran13 -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
set_mode()
to disable echo and line editing mode, and another C
routine next_char()
to read a single input character:
/* term.c */
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#ifdef __cplusplus
extern "C" {
#endif
void next_char(int *);
void set_mode(int *);
void next_char(int *ich)
{
*ich = getchar();
}
void set_mode(int *mode)
{
static struct termios termattr, saveattr;
if (*mode == 0)
{
tcsetattr(STDIN_FILENO, TCSADRAIN, &saveattr);
}
else
{
tcgetattr(STDIN_FILENO, &termattr);
saveattr = termattr;
termattr.c_lflag &= ~(ICANON | ECHO);
termattr.c_cc[VMIN] = 1;
termattr.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &termattr);
}
}
#ifdef __cplusplus
}
#endif
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_next_char(ich) bind(c, name='next_char')
import :: c_int
implicit none
integer(kind=c_int), intent(out) :: ich
end subroutine c_next_char
subroutine c_set_mode(mode) bind(c, name='set_mode')
import :: c_int
implicit none
integer(kind=c_int), intent(in) :: mode
end subroutine c_set_mode
end interface
integer :: ich
! Enable single key capture.
call c_set_mode(1)
print '("Press <q> to quit.")'
do
! Read single character.
call c_next_char(ich)
print '("Key pressed: ", i0)', ich
if (ich == iachar('q')) exit
end do
! Revert to default.
call c_set_mode(0)
end program main
Compile the example with GCC:
$ gcc13 -c term.c
$ gfortran13 -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
Optionally, install the library system-wide, for example, to
/opt
:
$ make install PREFIX=/opt
--- Installing libfortran-unix.a to /opt/lib/ ...
--- Installing module files to /opt/include/libfortran-unix/ ...
The following program is nearly identical to the example listed in section
termios, with the C part implemented in pure Fortran. The
terminal attributes are set through the interface bindings provided by the
unix
module.
The member c_lflag
of derived type term_attr
is of
unsigned 4-byte integer type tcflag_t
. Since unsigned integers are
not supported by Fortran, the value is first converted to an 8-byte signed
integer, modified, and then written back as unsigned 4-byte integer.
! key.f90
program main
use :: unix
implicit none
integer :: ich
! Enable single key capture.
call set_mode(1)
print '("Press <q> to quit.")'
do
! Read single character.
ich = next_char()
print '("Key pressed: ", i0)', ich
if (ich == iachar('q')) exit
end do
! Revert to default.
call set_mode(0)
contains
integer function next_char() result(ich)
ich = c_getchar()
end function next_char
subroutine set_mode(mode)
integer, intent(in) :: mode
integer :: stat
integer(kind=c_int64_t) :: c_lflag
type(c_termios) :: term_attr
type(c_termios), save :: save_attr
if (mode == 0) then
stat = c_tcsetattr(STDIN_FILENO, TCSADRAIN, save_attr)
else
stat = c_tcgetattr(STDIN_FILENO, term_attr)
save_attr = term_attr
c_lflag = c_uint_to_int(term_attr%c_lflag)
! Equivalent to: c_lflag &= ~(ICANON | ECHO);
c_lflag = iand(c_lflag, not(int(ior(ICANON, ECHO), kind=c_int64_t)))
term_attr%c_lflag = c_int_to_uint(c_lflag)
term_attr%c_cc(VMIN) = 1_c_cc_t
term_attr%c_cc(VTIME) = 0_c_cc_t
stat = c_tcsetattr(STDIN_FILENO, TCSANOW , term_attr)
end if
end subroutine set_mode
end program main
If the fortran-unix library is installed to /opt
,
compile and link the program with:
$ gfortran -I/opt/include/libfortran-unix -o key key.f90 /opt/lib/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
- C. Page: Reading single keystrokes from Fortran, 1994
< Inter-Process Communication | [Index] | Date and Time > |