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).
-
1Why not use something like `readline`, which gives you plenty of control over input processing? – Barmar Oct 13 '14 at 23:22
-
1Set the kill char to `ESC`? But be careful when you do so. You should reinstate the user's original preferences before your program exits. – Jonathan Leffler Oct 13 '14 at 23:27
-
readline already uses the Esc key for an elaborate set of purposes which are completely tangential to what I am trying to accomplish. My UI here is for data entry, not command processing, so having a complex line editor is undesirable; I just need the basic canonical behavior BUT with the escape key being handled. – Tyler Durden Oct 14 '14 at 00:46
3 Answers
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;
}

- 1
- 1

- 13,297
- 4
- 43
- 66
-
1Note that you should really keep a copy of the original value of the kill character and reinstate that after your program is finished and before it exits. You affect all processes, including the shell that invoked it — at least in principle. I suppose that with a modern shell that already dinks with terminal settings, it may well be 'OK' if the shell resets its settings — but you should at least verify that with `stty -a` or thereabouts. – Jonathan Leffler Jan 16 '15 at 03:47
-
@JonathanLeffler That's fair; I added code to restore the original settings. Highlighting, however, is broken; I don't know why but it's as if the whole site's L&F suddenly changed subtly. – Iwillnotexist Idonotexist Jan 16 '15 at 03:55
-
1If you're referring to SO's L&F, then yes, they've just today propagated the change that they made to Meta Stack Overflow a while ago (see [Feedback requested: Stack Overflow design update](http://meta.stackoverflow.com/questions/277200/feedback-requested-stack-overflow-design-update)) to the main Stack Overflow. Mostly it is good, but it is going to take a bit of getting used to. (See also [Is this flat theme new or is there an option to change it back?](http://meta.stackoverflow.com/questions/283630/is-this-flat-theme-new-or-is-there-an-option-to-change-it-back)) – Jonathan Leffler Jan 16 '15 at 03:57
-
@TylerDurden Have you had the opportunity to try out my solution and see if it works? If not, I could try to improve it. – Iwillnotexist Idonotexist Jan 17 '15 at 14:04
-
3Tyler asked the question in October 2014; he may well have moved past this obstacle and not have a simple way to decide whether it works for him or not. – Jonathan Leffler Jan 17 '15 at 16:40
-
1This is a useful technique, but the goal here is to cancel the input, not clear the command line. In other words the line needs to be returned to the program as though Enter was pressed, except since the last character is escape it will clue the app to know it is a cancel. – Tyler Durden Jan 23 '15 at 03:23
-
@TylerDurden I made an edit at the bottom that approximates what you want as closely as possible within the constraints of canonical mode. – Iwillnotexist Idonotexist Jan 23 '15 at 05:44
-
Aha, alternate EOL, I did not know about that. I had independently figured out that I could set ESC to EOF, then if the buffer includes the newline it is ENTER, and if it has no newline then it is ESC. However, your answer is just as good. Thanks. – Tyler Durden Jan 23 '15 at 15:06
You need to set the EOF
character to ESC
instead of Enter
using the tcsetattr()
function. For more detailed info visit http://pubs.opengroup.org/onlinepubs/7908799/xbd/termios.html#tag_008_001_009

- 13,297
- 4
- 43
- 66

- 627
- 2
- 8
- 19
This is a slightly modified version of my getLine()
function, used for robust input from the user. You can see details on the original here but this one has been modified to use the termios
stuff which allows a degree of control over input.
Because termios
works at a lower level than standard C input, it affects that as well.
First, the required headers and return values from the getLine()
function:
#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
Next, a helper function for reverting the terminal to its original state, this allows you to easily return a value from getLine()
knowing that the terminal will be left in its original state.
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;
}
Next, the actual getLine()
function itself, which modifies the terminal attributes to make ESC the kill character, then calls fgets()
along with all the extras for prompting, detecting buffer overflow, flushing input to the end of the line and so on.
During the time that user is within fgets()
as part of this function, the modified terminal behaviour is active and you can use ESC to clear the line.
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);
}
And finally, a test program so that you can check its behaviour. Basically, it will allow you to enter lines up to twenty characters then it will print them out with a status (too long, no input, etc).
If, at any time during the input process, you press ESC, it will kill the line and start again.
Entering exit
will cause the program to 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;
}