C: Detectando eventos no terminal

O C por padrão apresenta algumas funções para entrada e saída de dados. Entre elas, as mais famosas talvez sejam printf() e scanf(). Elas funcionam bem na maioria dos casos e são portáveis.

Entretanto, quando nós sabemos qual será nossa plataforma de desenvolvimento, nós podemos sair um pouco dos padrões* para adicionar algumas funcionalidades, como faremos agora.

Digamos que um software esteja sendo desenvolvido (para o terminal) e que em um dado momento, o usuário deve escolher uma opção, alguma coisa parecida com isto:

Houve um erro na hora de executar asdf:
[R]epetir    [C]ontinuar    [A]bortar

Usando getchar ou scanf ou qualquer outra função da biblioteca stdio.h, a leitura será feita linha por linha. Por isso, mesmo que nós queiramos apenas um caractere, o usuário terá que apertar esse caractere e depois Enter.

Eu queria uma forma de fazer isso em que o usuário simplesmente digitasse ‘r’ ou ‘c’, enfim, apenas uma tecla. Pensei em usar ncurses até, mas achei essa solução exagerada, e além disso, como o ncurses faz isso?

Pensando nisso, cheguei à biblioteca termios.h (na verdade eu já a havia utilizado no meu programa de criptografia usando xor). E junto a ela, usaremos funções de leitura e escrita de mais baixo nível, read e write.

*Com isso, nós saímos dos padrões do C, mas continuamos dentro dos padrões POSIX.

termios.h

Para que a idéia funcione, precisamos alterar algumas opções do terminal, e é aí que entra a biblioteca termios.h. Vamos definir duas funções, verbose(), que volta as configurações ao normal, e silent(), que tira o echo do terminal. O comportamento desejado é que quando o usuário aperte uma tecla, ela não apareça na tela, mas isso traz uma consequência: se você tentar usar o printf() ou qualquer outra função do tipo, ela também não aparecerá. Por isso, temos que ficar alternando entre os modos.

#include <termios.h>
void silent(struct termios *init_setting, struct termios *new_setting)
{
    tcgetattr(fileno(stdin), init_setting);
    new_setting = init_setting;

    new_setting->c_lflag &= ~(ICANON | ECHO);

    new_setting->c_cc[VMIN] = 1;
    new_setting->c_cc[VTIME] = 0;

    tcsetattr(fileno(stdin), TCSADRAIN, new_setting);
}

void verbose(struct termios *init_setting)
{
    tcsetattr(fileno(stdin), TCSADRAIN, init_setting);
}

O echo é desativado na linha “new_setting->c_lflag &= ~(ICANON | ECHO);“, onde nós também desligamos o modo canônico, o que nos permite fazer a leitura caractere por caractere, ao invés de linha por linha, como é o padrão.

Outro detalhe importante são as linhas new_setting->c_cc[]. A função read() é uma função que bloqueia a execução do programa até que certa condição seja encontrada. No caso, definimos que ela só pode retornar após ler 1 caractere (VMIN), não importa quanto tempo (em décimos de segundo) isso demore (VTIME). Neste exemplo, poderíamos também usar c_cc[VMIN] = 0 e c_cc[VTIME] = 100, que faz com que a função retorne assim que receber o primeiro caractere ou depois de 10 segundos, permitindo que aí o programa continue com uma opção padrão.

Usando

Agora veremos um exemplo de uso. Eu recomendo que se evite loops do tipo que há nesse código, exceto quando há uma função que bloqueie a execução, como é o caso de read(), e, conseqüentemente de get_char(), então não há problemas. Esse tipo de loop pode maximizar o uso de CPU sem estar fazendo nada, só esperando o usuário, por isso quando for necessário esse tipo de loop, procure usar nanosleep() ou usleep(); mas como já disse, não é esse o caso aqui.

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

#include <termios.h>

void silent(struct termios *init_setting, struct termios *new_setting);
void verbose(struct termios *init_setting);

unsigned char get_char(void);

int main(int argc, char *argv[])
{
    struct termios init_setting, new_setting;
    unsigned char ch = 0;

    printf("Houve um erro na execução de foo.\n[C]ontinuar\t[R]epetir\t[A]bortar\n");
    silent(&init_setting, &new_setting);

    while(ch != 'a' && ch != 'A')
    {
        ch = get_char();

        switch(ch)
        {
            case 'r':
            case 'R':
                verbose(&init_setting);
                printf("Repetindo...\n");
                
                printf("Houve um erro na execução de foo.\n[C]ontinuar\t[R]epetir\t[A]bortar\n");
                silent(&init_setting, &new_setting);
                break;

            case 'c':
            case 'C':
                verbose(&init_setting);
                printf("Continuando...\n");

                sleep(2);
                printf("Houve um erro na execução de foo.\n[C]ontinuar\t[R]epetir\t[A]bortar\n");
                silent(&init_setting, &new_setting);
                break;
        }
    }

    verbose(&init_setting);
    return(0);
}

void silent(struct termios *init_setting, struct termios *new_setting)
{
    tcgetattr(fileno(stdin), init_setting);
    new_setting = init_setting;

    new_setting->c_lflag &= ~(ICANON | ECHO | ISIG);

    new_setting->c_cc[VMIN] = 1;
    new_setting->c_cc[VTIME] = 0;

    tcsetattr(fileno(stdin), TCSADRAIN, new_setting);
}

void verbose(struct termios *init_setting)
{
    tcsetattr(fileno(stdin), TCSADRAIN, init_setting);
}

unsigned char get_char()
{
    unsigned char ch;

    read(fileno(stdin), &ch, sizeof(unsigned char));

    return ch;
}
Publicidade

Uma resposta para “C: Detectando eventos no terminal

  1. Pingback: C: Calculadora (simples) com notação polonesa reversa « Aletéia

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

%d blogueiros gostam disto: