Brincando com termios.h = getch() e kbhit() no Linux

Introdução

A interface termios é usada para se manipular as propriedades de um terminal. Nesse post veremos como utilizá-la para simularmos funções como getch(), getche() e khbit() no Linux.

A estrutura termios

Um terminal é representado pela seguinte estrutura:

    struct termios {
        tcflag_t    c_iflag;    /* input flags */
        tcflag_t    c_oflag;    /* output flags */
        tcflag_t    c_cflag;    /* control flags */
        tcflag_t    c_lflag;    /* local flags */
        cc_t        c_cc[NCCS]; /* control characters */
    };

Para nossos propósitos, usaremos somente os campos c_lflag e c_cc. No campo c_lflag ativaremos e desativaremos o modo canônico de processamento, como também o ecoamento de dados na tela. Já no campo c_cc, usaremos os atributos V_TIME e V_MIN.

Modo canônico vs não-canônico

Para que você entenda esses conceitos, considere o funcionamento de um shell interativo, ou seja, o fato do shell esperar o usuário digitar o comando para depois fazer o processamento. Toda tecla que o usuário aperta, uma interrupção é gerada no kernel. O kernel poderia muito bem passar essas teclas diretamente para o shell assim que elas são digitadas, mas isso seria um pouco eficiente já que o shell trabalha em linhas (um comando só é executado quando um ENTER é pressionado) e que também alguns erros de digitação poderiam ocorrer nesse processo. A solução para isso foi o próprio driver do dispositivo do terminal armazenar os bytes para formarem linhas e também realizar as correções dos dados de entrada (ex: backspace e delete). Nesse modelo, o driver cuida dos dados de entrada enquanto o shell simplesmente espera até que eles fiquem prontos.

Esse comportamento nada mais é que a definição de modo canônico: se um terminal é colocado em modo canônico, ele irá armazenar os dados de entrada em um buffer e realizará operações em cima desses dados. Esses dados só serão retornados para a aplicação quando um ENTER for pressionado.

Por outro lado, nem todos os programas funcionam como um shell interativo e por isso o terminal também funciona em modo não canônico. A diferença entre esses dois modos é o processo de bufferização: enquanto o modo canônico bufferiza os dados em linhas, o modo não canônico bufferiza os dados de acordo com dois parâmetros na estrutura termios, c_cc[VMIN] e c_cc[VTIME]. A ideia é que c_cc[VMIN] indique o mínimo de caracteres que entrará no buffer e o c_cc[VTIME] o máximo de tempo a se esperar até que os dados sejam lidos. É devido a esse comportamento não canônico que as funções getch*() e kbhit() são possíveis de serem implementadas.

Obtendo e setando atributos do terminal

A função tcgetattr() obtém os atributos de um terminal e os armazena numa struct termios.

struct termios old_attr ;

old_attr = tcgetattr(0, &old_attr) ;

O 0 (zero) é o descritor do terminal associado ao processo. O segundo parâmetro é o endereço que armazenará os atributos obtidos.

Por outro lado, para atualizarmos os atributos de um terminal, usamos a função tcsetattr(). Essa função deve ser chamada toda vez que um atributo novo deva ser inserido ou retirado do terminal.

struct termios old_attr ;
struct termios new_attr ;

old_attr = tcgetattr(0, &old_attr) ;

new_attr = old_attr ;
new_attr.c_flag =& ~ECHO ;

tcsetattr(0, &new_attr) ;

Nesse exemplo, o ecoamento de dados foi desligado, o que faz o terminal não mostrar os caracteres na tela assim que são digitados.

Simulando as funções getch() e getche()

Essas funções são bem comuns no ambiente Windows, embora não estejam no padrão ANSI. Podemos simulá-las no Linux apenas modificando atributos do terminal.

O primeiro passo é mudar o terminal para o modo não-canônico. Isso é feito com a seguinte linha:

 new_attr.c_lflag &= ~ICANON ;

No modo não canônico temos a liberdade de indicar um tempo para os bytes sejam lidos e também um valor mínimo de leitura.

new_attr.c_cc[VTIME]=0 ; /* sem tempo de duração */
new_attr.c_cc[VMIN]=1 ; /* leitura de um caractere */

VTIME recebe um tempo em décimos de segundos indicando quanto tempo irá durar a leitura dos dados. Quando esse tempo se esgotar, a leitura de dados terminará imediatamente.

Em VMIN indicamos quantos bytes serão lidos para que o terminal retorne. Como getch*() retorna assim que um byte é lido, colocamos o valor 1.

Por fim, só temos que ativar/desativar o ecoamento dos dados sendo isso o que diferencia essas duas funções.

/* para getch() */
new_attr.c_lflag &= ~ECHO;

/* para getche() */
new_attr.c_lflag &= ECHO;

Agora vamos juntar tudo em um só código:

/*
 * [dgetch.c]
 * Simula as funções getch() e getche() no Linux.
 *
 * [Autor]
 * Daemonio (Marcos Paulo Ferreira)
 * undefinido at gmail com
 * https://daemoniolabs.wordpress.com
 *
 * Versão 1.0, by daemonio @ Thu Dec 27 11:10:21 BRST 2012
 */
#include <stdio.h>
#include <unistd.h>
#include <termios.h>

void init_attr(void) ;
void close_attr(void) ;
int getch(void) ;
int getche(void) ;

struct termios old_attr, new_attr;

void init_attr(void) {
    /* Obtém as configurações atuais. */
    tcgetattr(0,&old_attr);
    new_attr=old_attr;

    /* Desliga modo canônico. */
    new_attr.c_lflag &=~ICANON ;

    /* Espera indefinidamente até
     * que um byte seja lido. */
    new_attr.c_cc[VTIME]=0 ;
    new_attr.c_cc[VMIN]=1 ;
}

/* Retorna configurações antigas. */
void close_attr(void) {
    tcsetattr(STDIN_FILENO,TCSANOW,&old_attr);
}

int getch(void) {
    int c ;

    /* Desliga ecoamento. */
    new_attr.c_lflag &= ~ECHO;

    /* Chama getchar() que realizará somente a
     * leitura de um byte, devido o terminal
     * não estar em modo canônico quando ela
     * é chamada. */
    tcsetattr(STDIN_FILENO,TCSANOW,&new_attr);
    c = getchar() ;
    tcsetattr(STDIN_FILENO,TCSANOW,&old_attr);

    return c ;
}

int getche(void) {
    int c ;

    /* Liga ecoamento. */
    new_attr.c_lflag &= ECHO;

    /* Chama getchar() que realizará somente a
     * leitura de um byte, devido o terminal
     * não estar em modo canônico quando ela
     * é chamada. */
    tcsetattr(STDIN_FILENO,TCSANOW,&new_attr);
    c = getchar() ;
    tcsetattr(STDIN_FILENO,TCSANOW,&old_attr);

    return c ;
}

int main(void) {
    int a, b ;

    init_attr() ;

    printf("getche(): ") ; a = getche() ;
    printf("getch(): ")  ; b = getch() ;

    printf("%x e %x\n", a, b) ;

    close_attr() ;

    return 0;
}

/* EOF */

Testando:

$ gcc -o dgetch dgetch.c
$ ./dgetch
getche(): Agetch(): 41 e 42

Simulando kbhit()

Essa função verifica se algo foi pressionado no teclado e retorna o código da tecla pressionada ou -1 se nada foi pressionado. Desse modo, podemos verificar quando o teclado está inativo ou não sem travar o programa como um todo na leitura dos dados.

A única diferença em relação ao código anterior é que agora passamos um valor zero em VMIN. Como indicado em [2], nessa situação, um EOF é retornado se os dados de entrada não estiverem disponíveis.

/*
 * [dkbhit.c]
 * Simula a função kbhit().
 *
 * [Autor]
 * Daemonio (Marcos Paulo Ferreira)
 * undefinido at gmail com
 * https://daemoniolabs.wordpress.com
 *
 * Versão 1.0, by daemonio @ Thu Dec 27 20:40:22 BRST 2012
 */
#include <stdio.h>
#include <unistd.h>
#include <termios.h>

void init_attr(void) ;
void close_attr(void) ;
int kbhit(void) ;

struct termios old_attr, new_attr;

void init_attr(void) {
    /* Obtém as configurações atuais. */
    tcgetattr(0,&old_attr);
    new_attr=old_attr;

    /* Desliga modo canônico. */
    new_attr.c_lflag &=~ICANON ;

    /* Desliga ecoamento. */
    new_attr.c_lflag &= ~ECHO;

    new_attr.c_cc[VTIME]=0 ;
    new_attr.c_cc[VMIN]=0 ;
}

/* Retorna configurações antigas. */
void close_attr(void) {
    tcsetattr(STDIN_FILENO,TCSANOW,&old_attr);
}

int kbhit(void) {
    int c ;

    tcsetattr(STDIN_FILENO,TCSANOW,&new_attr);
    c = getchar() ; /* retorna EOF se nada foi pressionado */
    tcsetattr(STDIN_FILENO,TCSANOW,&old_attr);

    return c ;
}

int main(void) {
    int flag_ler_nome = 0;
    char nome[20] ;

    init_attr() ;

    while(1) {
        flag_ler_nome = kbhit() ;

        if(flag_ler_nome != EOF) {
            printf("Digite seu nome: ") ;
            fgets(nome, sizeof nome, stdin) ;
        } 

        printf("** Outros processamentos **\n") ;
    }

    close_attr() ;

    return 0;
}

/* EOF */

Vamos verificar:

** Outros processamentos **
** Outros processamentos **
** Outros processamentos **
** Outros processamentos **
** Outros processamentos **

Digite seu nome: daemonio

** Outros processamentos **
** Outros processamentos **
** Outros processamentos **
** Outros processamentos **
** Outros processamentos **
** Outros processamentos **

Repare que o programa realiza “outros processamentos” quando o teclado está ocioso.

Referências

[1] General Terminal Interface by The IEEE and The Open Group (Acessado em: Dezembro/2012)
http://pubs.opengroup.org/onlinepubs/009696899/basedefs/xbd_chap11.html

[2] (Acessado em: Dezembro/2012)
http://www.lafn.org/~dave/linux/terminalIO.html

[3] Terminal Concepts in GNU/Linux by Charles M. “Chip” Coldwell (Acessado em: Dezembro/2012)
http://frank.harvard.edu/~coldwell/terminals/

3 pensamentos sobre “Brincando com termios.h = getch() e kbhit() no Linux

  1. Excelente post. Gostaria que mais pessoas se empenhasse em divulgar informações como este autor. A informação passada aqui foi de excelente qualidade, agora compreendi o porque não conseguimos ver a senha digitada no terminal do Linux, achei até um código semelhante e só fui entender depois de ler o post.

Deixe um comentário