How to detect key presses in a Linux C GUI program without prompting the user?

11,211

Solution 1

You have to modify terminal settings using termios. See Stevens & Rago 2nd Ed 'Advanced Programming in the UNIX Environment' it explains why tcsetattr() can return successfuly without having set all terminal characteristcs, and why you see what looks to be redundant calls to tcsetattr().

This is ANSI C in UNIX:

#include <sys/types.h>
#include <sys/time.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <termios.h>
#include <errno.h>

int checktty(struct termios *p, int term_fd)
{
    struct termios ck;
    return (
       tcgetattr(term_fd, &ck) == 0 &&
      (p->c_lflag == ck.c_lflag) &&
      (p->c_cc[VMIN] == ck.c_cc[VMIN]) &&
      (p->c_cc[VTIME] == ck.c_cc[VMIN])
    );
}


int
keypress(int term_fd)
{
     unsigned char ch;
   int retval=read(term_fd, &ch, sizeof ch);
   return retval;
}

int   /* TCSAFLUSH acts like fflush for stdin */
flush_term(int term_fd, struct termios *p)
{
   struct termios newterm;
   errno=0;
   tcgetattr(term_fd, p);  /* get current stty settings*/

   newterm = *p; 
   newterm.c_lflag &= ~(ECHO | ICANON); 
   newterm.c_cc[VMIN] = 0; 
   newterm.c_cc[VTIME] = 0; 

   return( 
       tcgetattr(term_fd, p) == 0 &&
       tcsetattr(term_fd, TCSAFLUSH, &newterm) == 0 &&
       checktty(&newterm, term_fd) != 0
   );
}
void 
term_error(void)
{
     fprintf(stderr, "unable to set terminal characteristics\n");
     perror("");                                                
     exit(1);                                                   
}


void
wait_and_exit(void)
{
    struct timespec tsp={0,500};  /* sleep 500 usec (or likely more ) */
    struct termios  attr;
    struct termios *p=&attr;
    int keepon=0;
    int term_fd=fileno(stdin);

    fprintf(stdout, "press any key to continue:");
    fflush(stdout);
    if(!flush_term(term_fd, p) )
       term_error();
    for(keepon=1; keepon;)
    {
        nanosleep(&tsp, NULL);
        switch(keypress(term_fd) )
        {
              case 0:
              default:
                 break;
            case -1:
                 fprintf(stdout, "Read error %s", strerror(errno));
                 exit(1);
                 break;
            case 1:       /* time to quit */
                 keepon=0;
                 fprintf(stdout, "\n");
                 break;                 
        } 
    }
    if( tcsetattr(term_fd, TCSADRAIN, p) == -1 && 
          tcsetattr(term_fd, TCSADRAIN, p) == -1 )
          term_error();
    exit(0);
}

int main()
{
      wait_and_exit();
      return 0;  /* never reached */
}

The nanosleep call is there to prevent the code from gobbling up system resources. You could call nice() and not use nanosleep(). All this does is sit and wait for a keystroke, then exit.

Solution 2

If you want to do that in a graphical application, you should use some libraries to do this.

Such a simple task can be easily done with whatever library (even low level ones like Xlib).

Just choose one and look for a tutorial that shows how to handle keyboard events.

Solution 3

no way with ANSI C. Look at ncurses lib.

Solution 4

Here’s code from /usr/src/bin/stty/key.c:

f_cbreak(struct info *ip)
{

        if (ip->off)
                f_sane(ip);
        else {
                ip->t.c_iflag |= BRKINT|IXON|IMAXBEL;
                ip->t.c_oflag |= OPOST;
                ip->t.c_lflag |= ISIG|IEXTEN;
                ip->t.c_lflag &= ~ICANON;
                ip->set = 1;
        }
}

At a minimum, you have to get out of ICANON mode before your select(2) syscall or your FIONREAD ioctl will work.

I have an ancient, 20-year-old perl4 program that clears CBREAK and ECHO mode this way. It is doing curses stuff without resorting to the curses library:

sub BSD_cbreak {
    local($on) = shift;
    local(@sb);
    local($sgttyb);
    # global $sbttyb_t 

    $sgttyb_t = &sgttyb'typedef() unless defined $sgttyb_t;

    # native BSD stuff by author (tsc)

    ioctl(TTY,&TIOCGETP,$sgttyb)
        || die "Can't ioctl TIOCGETP: $!";

    @sb = unpack($sgttyb_t,$sgttyb);
    if ($on) {
        $sb[&sgttyb'sg_flags] |= &CBREAK;
        $sb[&sgttyb'sg_flags] &= ~&ECHO;
    } else {
        $sb[&sgttyb'sg_flags] &= ~&CBREAK;
        $sb[&sgttyb'sg_flags] |= &ECHO;
    }
    $sgttyb = pack($sgttyb_t,@sb);
    ioctl(TTY,&TIOCSETN,$sgttyb)
            || die "Can't ioctl TIOCSETN: $!";
}


sub SYSV_cbreak {
    # SysV code contributed by Jeff Okamoto <[email protected]>

    local($on) = shift;
    local($termio,@termio);
    # global termio_t ???

    $termio_t = &termio'typedef() unless defined $termio_t;

    ioctl(TTY,&TCGETA,$termio)
       || die "Can't ioctl TCGETA: $!";

    @termio = unpack($termio_t, $termio);
    if ($on) {
        $termio[&termio'c_lflag] &= ~(&ECHO | &ICANON);
        $termio[&termio'c_cc + &VMIN] = 1;
        $termio[&termio'c_cc + &VTIME] = 1;
    } else {
        $termio[&termio'c_lflag] |= (&ECHO | &ICANON);

        # In HP-UX, it appears that turning ECHO and ICANON back on is
        # sufficient to re-enable cooked mode.  Therefore I'm not bothering
        # to reset VMIN and VTIME (VEOF and VEOL above).  This might be a
        # problem on other SysV variants.

    }
    $termio = pack($termio_t, @termio);
    ioctl(TTY, &TCSETA, $termio)
        || die "Can't ioctl TCSETA: $!";

}


sub POSIX_cbreak {
    local($on) = shift;
    local(@termios, $termios, $bitmask);

    # "file statics" for package cbreak:
    #      $savebits, $save_vtime, $save_vmin, $is_on

    $termios_t = &termios'typedef() unless defined $termios_t;
    $termios = pack($termios_t, ());  # for Sun SysVr4, which dies w/o this

    ioctl(TTY,&$GETIOCTL,$termios)
        || die "Can't ioctl GETIOCTL ($GETIOCTL): $!";

    @termios = unpack($termios_t,$termios);

    $bitmask  = &ICANON | &IEXTEN | &ECHO;
    if ($on && $cbreak'ison == 0) {
        $cbreak'ison = 1;
        $cbreak'savebits = $termios[&termios'c_lflag] & $bitmask;
        $termios[&termios'c_lflag] &= ~$bitmask;
        $cbreak'save_vtime = $termios[&termios'c_cc + &VTIME];
        $termios[&termios'c_cc + &VTIME] = 0;
        $cbreak'save_vmin  = $termios[&termios'c_cc + &VMIN];
        $termios[&termios'c_cc + &VMIN] = 1;
    } elsif ( !$on && $cbreak'ison == 1 ) {
        $cbreak'ison = 0;
        $termios[&termios'c_lflag] |= $cbreak'savebits;
        $termios[&termios'c_cc + &VTIME] = $cbreak'save_vtime;
        $termios[&termios'c_cc + &VMIN]  = $cbreak'save_vmin;
    } else {
        return 1;
    } 
    $termios = pack($termios_t,@termios);
    ioctl(TTY,&$SETIOCTL,$termios)
        || die "Can't ioctl SETIOCTL ($SETIOCTL): $!";
}

sub DUMB_cbreak {
    local($on) = shift;

    if ($on) {
        system("stty  cbreak -echo");
    } else {
        system("stty -cbreak  echo");
    }
} 

And it elsewhere says that for POSIX,

    ($GETIOCTL, $SETIOCTL)  = (TIOCGETA, TIOCSETA); 

RE-translation back into the original C is left as an exercise for the reader, because I can't remember where the 20-years-ago-me snagged it from originally. :(

Once you're out of ICANON mode on the tty, now your select(2) syscall works properly again. When select's read mask returns that that descriptor is ready, then you do a FIONREAD ioctl to discover exactly how many bytes are waiting for you on that file descriptor. Having got that, you can do a read(2) syscall for just that many bytes, preferably on an O_NONBLOCK descriptor, although by now that should no longer be necessary.

Hm, here’s a foreboding note in /usr/src/usr.bin/vi/cl/README.signal:

    Run in cbreak mode.  There are two problems in this area.  First, the
    current curses implementations (both System V and Berkeley) don't give
    you clean cbreak modes. For example, the IEXTEN bit is left on, turning
    on DISCARD and LNEXT.  To clarify, what vi WANTS is 8-bit clean, with
    the exception that flow control and signals are turned on, and curses
    cbreak mode doesn't give you this.

    We can either set raw mode and twiddle the tty, or cbreak mode and
    twiddle the tty.  I chose to use raw mode, on the grounds that raw
    mode is better defined and I'm less likely to be surprised by a curses
    implementation down the road.  The twiddling consists of setting ISIG,
    IXON/IXOFF, and disabling some of the interrupt characters (see the
    comments in cl_init.c).  This is all found in historic System V (SVID
    3) and POSIX 1003.1-1992, so it should be fairly portable.

If you do a recursive grep for \b(TIOC[SG]ET[NP]|TC[SG]ET[SA]|tc[sg]etattr)\b on the non-kernel portions of /usr/src/, you should find stuff you can use. For example:

% grep -Pr '\b(TIOC[SG]ET[NP]|TC[SG]ET[SA]|tc[sg]etattr)\b' /usr/src/{{s,}bin,usr.{s,}bin,etc,gnu}

I would look at /usr/src/usr.bin/less/screen.c, down in the raw_mode() function. Riddled with ifdefs though it is in a quest for portability, that looks like the cleanest code for what you want to do. There’s also stuff lurking down in GNU.

OH MY, look in /usr/src/gnu/usr.bin/perl/h2pl/cbreak.pl! That must be my old code that I posted above. Interesting that it’s trickled out to every src system in the world. Scary, too, since it is twenty years out of date. Gosh, it's weird to see echoes of a younger self. Really hard to remember such particulars from 20 years ago.

I also see in /usr/src/lib/libcurses/term.h this line:

#define tcgetattr(fd, arg) ioctl(fd, TCGETA, arg)

in a bunch of ifdefs that are trying to infer termio or termios availability.

This should be enough to get you started.

Share:
11,211
Pooja N Babu
Author by

Pooja N Babu

Updated on June 13, 2022

Comments

  • Pooja N Babu
    Pooja N Babu almost 2 years

    how to detect a keyboard event in C without prompting the user in linux? That is the program running should terminate by pressing any key. can anyone please help with this?