Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make arrow keys and backspace work correctly when asking input from user in C program using termios.h?

So I have the following code which basically just reads characters user inputs and prints them until 'q' is entered.

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<termios.h>

int main(void) {
    char c; 
    static struct termios oldtio, newtio;
    tcgetattr(0, &oldtio);
    newtio = oldtio;
    newtio.c_lflag &= ~ICANON;
    newtio.c_lflag &= ~ECHO;
    tcsetattr(0, TCSANOW, &newtio);

    printf("Give text: ");
    fflush(stdout);
    while (1) {
        read(0, &c, 1);
        printf("%c", c);
        fflush(stdout);
        if (c == 'q') { break; }
    }
    printf("\n"); 
    tcsetattr(0, TCSANOW, &oldtio);

    return 0;
}

In the beginning of the main function I turn off the canonical mode to make user able to see his input when he's giving it. I also turn off the echo so stuff like "^[[A" doesn't pop up when pressing the up arrow key for example. This works, but I'm also able to move the cursor to upper rows on a terminal window and that's not good. Is there a way fix this so that user can only move within the current row?

Another problem is the backspace. When I press it the program prints a weird symbol (which I assume is 0x7f) instead of erasing the character left to the cursor's current location. I should propably handle the backspace key output in the program somehow but I don't know how to do it since it's this weird hexadecimal number. Any tips for this?

One option I've also been thinking about to make this work is to use canonical mode so the arrow keys and backspace functionalities are automatically in use. However, canonical mode works line by line and so the text doesn't appear until "Enter" is hit by the user. So far, I haven't figured out any way to make user see his input while typing. Is this even possible?

And please, no ncurses or readline suggestions. I want to do this using termios.h.

like image 997
JZ555 Avatar asked Oct 28 '14 17:10

JZ555


1 Answers

have you looked into the man pages? (should be man termios or look somewhere online)

There I found the ECHOE flag which is said to have the following effect:

If ICANON is also set, the ERASE character erases the preceding input character, and WERASE erases the preceding word.

This should fix your backspace problem?

I also suggest, you have a look into the examples in the man page. E.g. you could do the following:

newtio.c_lflag &= ~(ECHO | ECHOE | ICANON);

...to set more than one flag at a time in only one line. I know the man pages are hard to read for beginners but you will get used to them and the more you use them, the more efficient they become for looking up C/POSIX-functions etc (just in case, you don't use them anyway).

The arrow-key-problem: Maybe you can try the cfmakeraw()-function; its description sounds promising. I haven't had time to investigate any further about the arrow keys. However, maybe you find something else useful in the man page.

BTW: termios looks interesting, I always wondered which functions certain command line programmes are using; learned something by your question, thanks!

EDIT

I've done some more research this weekend. The "strange" symbol printed when pressing the backspace key is quite easy to hide. It is the ASCII-value 0x7f. So add a simple

if (c == 0x7f) { continue; }

...to just ignore the backspace key. Or handle it in a way to remove the last character (see code example below).

This simple workaround doesn't work for the arrow keys though as they are no ASCII characters :/ However, these two topics helped me handling also this problem: topic 1 and topic 2. Basically pressing the arrow keys results in a sequence of a couple of chars being sent to stdin (see the second link for more information).

Here is my complete code which (I think) works the way you wish:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <termios.h>

int main(void) {
    char c;
    static struct termios oldtio, newtio;
    tcgetattr(0, &oldtio);
    newtio = oldtio;
    newtio.c_lflag &= ~ICANON;
    newtio.c_lflag &= ~ECHO;
    tcsetattr(0, TCSANOW, &newtio);

    printf("Give text:\n");
    fflush(stdout);
    while (1) {
        c = getchar();

        // is this an escape sequence?
        if (c == 27) {
            // "throw away" next two characters which specify escape sequence
            c = getchar();
            c = getchar();
            continue;
        }

        // if backspace
        if (c == 0x7f) {
            // go one char left
            printf("\b");
            // overwrite the char with whitespace
            printf(" ");
            // go back to "now removed char position"
            printf("\b");
            continue;
        }

        if (c == 'q') {
            break;
        }
        printf("%c", c);
    }
    printf("\n");
    tcsetattr(0, TCSANOW, &oldtio);

    return 0;
}

BTW you can get the complete escape sequences by the following code:

int main(void) {
    char c;
    while (1) {
        c = getchar();
        printf("%d", c);
    }
    return 0;
}

I think I don't have to say that this complete thing is quite a dirty hack and it's easy to forget handling some special keys. E.g. in my code I don't handle the page-up/down or home keys... --> the code above is far from complete but gives you a point to start. You should also have a look at terminfo which can provide you a lot of the necessary information; it should also help with a more portable solution. As you see, this "simple" thing can become quite complex.So you might rethink your decision against ncurses :)

like image 60
mozzbozz Avatar answered Oct 29 '22 16:10

mozzbozz