5

In unix plain C termios programming, if I am using canonical mode to receive a line of input from the user, how can I process the escape key? In general, if the user is entering a line of text and presses escape nothing happens. I would like to cancel the current input if the user presses escape. I know I can process individual characters, but then I lose all the benefits of canonical mode (backspaces, etc).

4

3 回答 3

4

Original

With all due credits to Jonathan Leffler for his comment that hinted me at the right direction, at the bottom is my annotated first termios program demonstrator (Thanks!).

The key is to use tcgetattr(ttyfd, &attributes) on the current terminal's file descriptor to retrieve its current attributes into in a struct termios, edit the attributes, then apply the changes with tcsetattr(ttyfd, when, &attributes).

One of the attributes is the "kill" character - the character that causes the entire currently-buffered line to be discarded. It is set by indexing into the c_cc member array of struct termios and setting attr.c_cc[VKILL] to whatever one wants (Here, to Esc, which is equal to octal 033).

The kill character should be restored to its previous value on exit.

#include <termios.h>
#include <fcntl.h>
#include <stdio.h>


int main(){
    char   buf[80];
    int    numBytes;
    struct termios original, tattered;
    int    ttyfd;
    
    /* Open the controlling terminal. */
    ttyfd = open("/dev/tty", O_RDWR);
    if(ttyfd < 0){
        printf("Could not open tty!\n");
        return -1;
    }
    
    /**
     * Get current terminal properties, save them (including current KILL char),
     * set the new KILL char, and make this the new setting.
     */
    
    tcgetattr(ttyfd, &original);
    tattered = original;
    tattered.c_cc[VKILL] = 033;/* New killchar, 033 == ESC. */
    tcsetattr(ttyfd, TCSANOW, &tattered);
    
    /**
     * Simple test to see whether it works.
     */
    
    write(1, "Please enter a line: ", 21);
    numBytes = read(0, buf, sizeof buf);
    write(1, buf, numBytes);
    
    /**
     * Restore original settings.
     */
    
    tcsetattr(ttyfd, TCSANOW, &original);
     
    /* Clean up. */
    close(ttyfd);
    
    return 0;
}

This demo appears to work on Mac OS X 10.6.8. I've also tested this on Linux, and apparently Esc to kill the buffer appears to be the default, as if I print out c_cc[VKILL] I obtain 27 == 033 == ESC.

Edit

The below attempts as closely as possible to imitate the behaviour you described in your comment. It sets c_cc[VEOL2] to Esc; EOL2 is the alternate End-of-Line. It also removes Esc as the kill character, since you want to receive the line.

What now happens is that if a normal Ret is pressed, all is normal. However, if Esc is pressed, the last character in the buffer is set to Esc, a condition which may be tested (although only after reading and buffering the whole line first).

Below is a demonstrator according to your clarified specs. It waits for a line of input and echoes it back with

  • <CANCELLED> if the line was terminated with Esc and
  • <NORMAL > if the line was terminated with Ret.

Enjoy!

#include <termios.h>
#include <fcntl.h>
#include <stdio.h>


int main(){
    char   buf[80];
    int    numBytes;
    struct termios original, tattered;
    int    ttyfd;

    /* Open the controlling terminal. */
    ttyfd = open("/dev/tty", O_RDWR);
    if(ttyfd < 0){
        printf("Could not open tty!\n");
        return -1;
    }

    /**
     * Get current terminal properties, save them (including current KILL char),
     * set the new KILL char, and make this the new setting.
     */

    tcgetattr(ttyfd, &original);
    tattered = original;
    tattered.c_cc[VKILL] = 0;  /* <Nada> */
    tattered.c_cc[VEOL2] = 033;/* Esc */
    tcsetattr(ttyfd, TCSANOW, &tattered);

    /**
     * Simple test to see whether it works.
     */

    fputs("Please enter a line: ", stdout);
    fflush(stdout);
    numBytes = read(0, buf, sizeof buf);
    if(buf[numBytes-1]==033){/* Last character is Esc? */
        buf[numBytes-1] = '\n';/* Substitute with newline */
        fputs("\n<CANCELLED> ", stdout);   /* Print newline to move to next line */
    }else{
        fputs("<NORMAL   > ", stdout);
    }
    fwrite(buf, 1, numBytes, stdout);

    /**
     * Restore original settings.
     */

    tcsetattr(ttyfd, TCSANOW, &original);

    /* Clean up. */
    close(ttyfd);

    return 0;
}
于 2015-01-16T01:51:09.203 回答
2

这是我的getLine()函数的一个稍微修改的版本,用于来自用户的稳健输入。您可以在此处查看原始版本的详细信息,但此版本已被修改为使用termios允许对输入进行一定程度控制的东西。

因为termios工作在比标准 C 输入更低的级别,所以它也会影响它。

首先,函数所需的标头和返回值getLine()

#include <termios.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

#define OK        0
#define NO_INPUT  1
#define TOO_LONG  2
#define TERM_PROB 3

接下来,一个用于将终端恢复到其原始状态的辅助函数,这使您可以轻松地返回一个值,因为您getLine()知道终端将保持其原始状态。

static int revertTerm (int fd, struct termios *ptio, int old, int rc) {
    // Revert the terminal to its original state then return
    // specified value.

    ptio->c_cc[VKILL] = old;
    tcsetattr (fd, TCSANOW, ptio);
    close (fd);
    return rc;
}

接下来是实际getLine()函数本身,它修改终端属性以生成终止ESC字符,然后调用fgets()所有附加功能以进行提示、检测缓冲区溢出、将输入刷新到行尾等。

在用户fgets()作为此功能的一部分期间,修改后的终端行为处于活动状态,您可以使用ESC清除线路。

static int getLine (char *prmpt, char *buff, size_t sz) {
    int old, fd, ch, extra;
    struct termios tio;

    // Modify teminal so ESC is KILL character.

    fd = open ("/dev/tty", O_RDWR);
    if (fd < 0)
        return TERM_PROB;

    tcgetattr (fd, &tio);
    old = tio.c_cc[VKILL];
    tio.c_cc[VKILL] = 0x1b;
    tcsetattr (fd, TCSANOW, &tio);

    // Get line with buffer overrun protection.

    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }
    if (fgets (buff, sz, stdin) == NULL)
        return revertTerm (fd, &tio, old, NO_INPUT);

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.

    if (buff[strlen(buff)-1] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return revertTerm (fd, &tio, old, (extra == 1) ? TOO_LONG : OK);
    }

    // Otherwise remove newline and give string back to caller.

    buff[strlen(buff)-1] = '\0';
    return revertTerm (fd, &tio, old, OK);
}

最后是一个测试程序,以便您检查它的行为。基本上,它允许您输入最多 20 个字符的行,然后将它们打印出来并带有状态(太长、没有输入等)。

如果在输入过程中的任何时候按ESC,它将终止该行并重新开始。

输入exit将导致程序退出。

// Test program for getLine().

int main (void) {
    int rc, done = 0;
    char buff[21];

    while (!done) {
        rc = getLine ("Enter string (ESC to clear, exit to stop)> ",
            buff, sizeof(buff));
        if (rc == NO_INPUT) {
            // Extra NL since my system doesn't output that on EOF.
            printf ("\nNo input\n");
        } else if (rc == TOO_LONG) {
            printf ("Input too long [%s]\n", buff);
        } else {
            done = (strcmp (buff, "exit") == 0);
            if (!done)
                printf ("OK [%s]\n", buff);
        }
    }

    return 0;
}
于 2015-01-21T08:30:47.623 回答
2

您需要将EOF字符设置为ESC而不是Enter使用该tcsetattr()功能。有关更多详细信息,请访问http://pubs.opengroup.org/onlinepubs/7908799/xbd/termios.html#tag_008_001_009

于 2015-01-15T23:59:09.657 回答