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/
O tempo dado em VMIN só é contado na leitura entre bytes, ou seja, o tempo só começa a decorrer depois que o primeiro byte é lido.
Muito bom o tutorial, foi o único satisfatório que encontrei nas buscas do Google. O fato de estar em português também é ótimo. Obrigado !
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.