Mampac
Mampac

Reputation: 351

Forcing a terminal not to print Ctrl hotkeys when signals are caught

Good day,

I'm writing my own shell in C for my school which has to resemble bash as closely as possible.

I have to handle signals such as Ctrl-\ and Ctrl-C as bash does; for this reason I'm allowed to use signal function. It works fine, but the thing is whenever a Ctrl-C signal is caught (starting from the second catch), a ^C is printed.

On the net, I've found a workaround suggesting printing "\b \b\b \b\nminishell$ " whenever a Ctrl-C is caught, which will devour the two symbols. The thing is, since at the very first time ^C is not printed, the print devours two symbols of my prompting, making it just minishell instead of minishell$ , with the cursor incorrectly displayed.

Now I've come up with another workaround for this workaround which is to declare a static boolean to not print the baskspaces at the very first call. This doesn't help in case of Ctrl-\ though; Ctrl-\ proceeds to move my cursor to right when I attempt to write the two whitespaces that must replace the ^\.

I don't like these workarounds and would like to know whether there is a way to instruct the terminal not to output this stuff? I'm allowed to use tgetent, tgetflag, tgetnum, tgetstr, tgoto, tputs, tcsetattr, tcgetattr, have read their man pages but nothing seems to be helpful.

Upvotes: 5

Views: 2239

Answers (3)

Sir Jo Black
Sir Jo Black

Reputation: 2096

The way to set the console such a SW may intercept all typed chars is to set the terminal in RAW MODE. The problems this way may present are that all keys that aren't in the ASCII 0-255 space, such as è, ì, à will be received from the console as a bytes sequence and all the function and control keys included cursors and backspace will not accomplish any action, some code such as CR, LF and some ANSI sequence may accomplish actions when are read from the input channel and rewritten on the output channel.

To set the terminal in raw mode you have to use the function cfmakeraw followed by the function tcsetattr.

The code below implements a simple but not very good implemented terminal, anyway I think this code is a good point to start. In any case, the code flow and the error control must be at least better arranged.

The code writes all sequence of ASCII char that enter into the console when a key is typed. All chars that have value smaller then 32 or greater then 126 will be written as [HEX-CODE]

I.E. hitting Esc on the console will be written [1B], the code of Ctrl+C will be written as [03], F1 will be [1B]OP, F11 will be [1B][23~, Enter will be [0D].

If you will hit Ctrl+X [18] will be written and the program stops, but this behaviour is under SW control as you can see in the code.

Here the code:

#include <stdio.h>      // Standard input/output definitions
#include <string.h>     // String function definitions
#include <unistd.h>     // UNIX standard function definitions
#include <fcntl.h>      // File control definitions
#include <errno.h>      // Error number definitions
#include <termios.h>    // POSIX terminal control definitions (struct termios)

#include <sys/ioctl.h> // Used for TCGETS2, which is required for custom baud rates
#include <sys/select.h> // might be used to manage select

int setAttr(int ch, int resetToOld);

#define IN 0
#define OUT 1

typedef struct TermCap
{
    int fd;
    struct termios oldTermios;
    struct termios newTermios;
    // fd_set fds; // might be used to manage select
} TermCap;

TermCap m_termCap[2];

int main()
{
    int i,ex=0;
    char msg;
    char buff[20];

    m_termCap[IN].fd=STDIN_FILENO;
    m_termCap[OUT].fd=STDOUT_FILENO;

    // Gets STDIN config and set raw config
    setAttr(IN,0);

    // Gets STDOUT config and set raw config
    setAttr(OUT,0);

    // Console loop ... the console terminates when ^X is intercepted.
    do {
        do {
            i=read(m_termCap[IN].fd,&msg,1);
            if (i>0){
                if (msg<32 || msg>126) {
                    sprintf(buff,"[%02X]",(unsigned char)msg);
                    write(m_termCap[OUT].fd,buff,4);
                    if (msg==24)
                        ex=1;
                }else{
                    write(m_termCap[OUT].fd,&msg,i);
                }
            }
            usleep(10000); // a minimal delay of 10 millisec
        } while(i>0 && !ex);
    } while(!ex);

    // Reset console to initial state.
    setAttr(IN,1);
    setAttr(OUT,1);

    printf("\r\n\nThe end!");
    return 0;
}

int setAttr(int ch, int resetToOld)
{
    int retVal=0;
    int i;

    if (!resetToOld) {
        // Read old term config
        i=tcgetattr(m_termCap[ch].fd, &m_termCap[ch].oldTermios);
        if (i==-1) {
            return 1;
        }
    }

    m_termCap[ch].newTermios = m_termCap[ch].oldTermios;

    if (!resetToOld) {
        // Terminal in raw mode
        cfmakeraw(&m_termCap[ch].newTermios);
    }

    i=tcsetattr(m_termCap[ch].fd, TCSANOW, &m_termCap[ch].newTermios);

    if (i==-1) {
        retVal = 2;
    }

    return retVal;
}
 

Upvotes: 1

wildplasser
wildplasser

Reputation: 44250

When you type a key on a terminal, two things happen

  • the character is echoed (displayed) on this terminal
  • the character is sent (over the line) to the attached program

Both these actions can be controlled via termios/tcsetattr(): a different character(s) can be sent or echoed, some can be suppressed, etc. (some/most of these actions take place in the terminal-driver , but this is not relevant here)

Demonstration: using tcsetattr() to control the echoing of the terminal:


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

#define _SVID_SOURCE 1
#include <termios.h>
#include <unistd.h>
#include <signal.h>

struct termios termios_save;

void reset_the_terminal(void)
{
tcsetattr(0, 0, &termios_save );
}

sig_atomic_t the_flag = 0;
void handle_the_stuff(int num)
{
char buff[4];
buff[0] = '[';
buff[2] = '0' + num%10;
num /= 10;
buff[1] = '0' + num%10;
buff[3] = ']';
write(0, buff, sizeof buff);
the_flag = 1;
}

int main (void)
{
int rc;
int ch;
struct termios termios_new;

rc = tcgetattr(0, &termios_save );
if (rc) {perror("tcgetattr"); exit(1); }

rc = atexit(reset_the_terminal);
if (rc) {perror("atexit"); exit(1); }

termios_new = termios_save;
termios_new.c_lflag &= ~ECHOCTL;
rc = tcsetattr(0, 0, &termios_new );
if (rc) {perror("tcsetattr"); exit(1); }

signal(SIGINT, handle_the_stuff);

printf("(pseudoshell)Start typing:\n" );
while(1) {
        ch = getc(stdin);
        if (the_flag) {
                printf("Saw the signal, last character was %02x\n", (unsigned) ch);
                break;
                }
        }

exit (0);
}

Upvotes: 6

user12184817
user12184817

Reputation:

Wouldn't this work?

void signalHandler(int signo){
 if(signo==SIGINT){
  printf("\b\b  \b\b");
  fflush(NULL);
  printf("\nHello World\n");    
 }
}

In my shell it seems to work fine. The first printf and fflush is what you have to implement in your handler. The printf after that is just a way for me to show you that you can, then, do whatever you want after the ^C not appearing.

Why does this make it not appear? In the first printf I erase the characters by using backspaces and spaces. As stdout is buffered by default and I didn't want to use a newline character, I flushed the buffer manually.

Upvotes: 0

Related Questions