Durante o feriado do carnaval, decidi brincar um pouco mais com o bash. Desta vez, descobri como detectar se as teclas direcionais do teclado foram pressionadas pelo usuário, o que, combinado às técnicas apresentadas no meu post anterior, me permitiu desenvolver um script que apresenta um menu com diversas opções sobre as quais o usuário pode navegar.
Seqüências de escape são “o ouro”, como dizem! Elas permitem realizar uma série de coisinhas legais, como mudar a posição do cursor (o clássico gotoxy) ou limpar a linha atual do terminal. Para quem não sabe, uma seqüência de escape é uma série de caracteres normalmente iniciada após pressionar a tecla ESC que permite que uma máquina ou aplicação execute um comando [1]. No escopo deste post, estamos falando das seqüências de escape ANSI X3.64 [2], usadas pela maioria dos terminais presentes nas distribuições Linux.
Para fazer um pequeno teste, experimente o seguinte: abra um terminal e pressione ENTER algumas vezes. Agora, digite o seguinte:
echo -e "\\e[2;1H\e[2Khello, world"
O cursor moveu-se para o início da segunda linha do terminal, e foi impressa a mensagem "hello, world". Explicando o comando:
echo - comando para imprimir mensagens no terminal. A opção -e permite que sejam usadas seqüências de escape.
\e[2;1H - esta sequencia de escape (\e representa o ESC) move o cursor para a primeira linha da segunda coluna.
\e[2K - seqüência de escape que limpa a linha atual.
hello, world - a mensagem a ser impressa
As seqüências de escape também permitem alterar a cor da fonte de do fundo do terminal. O comando abaixo, por exemplo, imprime a mensagem "hello, world" com fundo azul e letra branca:
echo -e "\\e[44;37mhello, world"
Além de brincar com as seqüências de escape, para escrever o script abaixo tive que descobrir como ler as tecla direcionais. Para isso, basta executar o comando read e pressionar alguma das teclas e ver o que é impresso. No caso da tecla direcional para cima, por exemplo, podemos ver que a string ^[[A é impressa no terminal. Seguindo esta dica, aprendi que, na verdade, ^[ também é o mesmo que a tecla ESC. Portanto, se quisermos "imprimir" uma tecla direcional para cima, podemos executar o comando
echo -e '\\e[A'
Não há muita utilidade em "imprimir" uma tecla direcional (normalmente é impresso um retangulo), mas é possível usar o comando acima para armazenar o valor da tecla em uma variável, para posteriores comparações:
UP_KEY=$(echo -n -e '\\e[A')
Abaixo está o script sobre o qual eu falei. Vale ressaltar que os echo's usam a opção -n para evitar que seja emitido um caractere de nova linha, o que o comando echo faz por padrão. As técnicas do post anterior foram utilizadas por apenas um motivo: o comando read é usado sem a opção -n. Fiz isso porque as teclas direcionais emitem 3 caracteres por vez, enquanto caracteres normais emitem apenas 1. Portanto, o comando read -n1 não funcionaria para detectar teclas direcionais, porém read -n3 obrigaria o usuário a teclar caso eu quisesse detectar outras teclas. Não é o caso deste script, mas pode ser que alguém queira permitir esse tipo de interação. Se a preocupação for apenas ler teclas direcionais, é possível usar o comando stty apenas com a opção -echo (para que não sejam impressas coisas estranhas na tela quanto o usuário tenta navegar além das opções apresentadas) e o comando read pode ser usado com a opção -n3.
Use as teclas direcionais para cima e para baixo para navegar entre as opções. Use a tecla direcional para a direita para escolher uma opção (apenas "Quit" faz alguma coisa neste script). Meu próximo passo será tentar descobrir como ler a barra de espaço e/ou a tecla ENTER (é preciso descobrir como determinar se uma dessas teclas foi pressionadas ou se nada foi pressionado).
Finalmente, eis o script:
#!/bin/bash
# armazena os valores das setas direcionais
UP_KEY=$(echo -n -e '\\e[A')
DOWN_KEY=$(echo -n -e '\\e[B')
RIGHT_KEY=$(echo -n -e '\\e[C')
# configurações do menu
OPTION=( "Option 1" "Option 2" "Option 3" "Quit" )
NUM_OPTIONS=${#OPTION[*]}
CURRENT=0
# vai para o início da linha atual. como não há uma seqüência de escape única
# para isso, o cursor é movido para o início da linha anterior e a seguir
# para o início da linha seguinte, retornando à linha atual
function startline () {
echo -n -e "\\e[1F"
echo -n -e "\\e[1E"
}
# limpa a linha atual
function clearline () {
echo -n -e "\\e[2K"
startline
}
# imprime uma opção do menu. se o segunto argumento for um valor verdadeiro
# (1, por exemplo), a opção é impressa como selecionada
function print_option () {
if [ $2 ]; then
echo -n -e "\\e[7m"
fi
clearline
echo -n -e ${OPTION[$1]}
echo -n -e "\\e[0m"
}
# move a seleção para cima. a sequencia de escape \\e[1F move o cursor para
# o início da linha anterior
function move_up () {
if [ $CURRENT -gt 0 ]; then
print_option $CURRENT
CURRENT=$((CURRENT - 1))
echo -n -e "\\e[1F"
print_option $CURRENT 1
fi
}
# move a seleção para baixo. a sequencia de escape \\e[1E move o cursor para
# o início da linha seguinte
function move_down () {
if [ $CURRENT -lt $((NUM_OPTIONS-1)) ]; then
print_option $CURRENT
CURRENT=$((CURRENT + 1))
echo -n -e "\\e[1E"
print_option $CURRENT 1
fi
}
# detecta se uma tecla foi pressionada
function keypressed () {
read KEY
[ $KEY ]
}
# armazena as configurações de linha do terminal
STTY_SETTINGS=$(stty -g)
# imprime as opções do menu
for i in $(seq 0 $((${#OPTION[*]}-1))); do
echo ${OPTION[i]}
done
# vai para a primeira opção e a imprime como selecionada
echo -n -e "\\e[${NUM_OPTIONS}F"
print_option $CURRENT 1
# altera as configurações de linha do terminal
stty -icanon min 0 time 0 -echo
# loop do menu
while [ 1 ]; do
if keypressed; then
if [ $KEY = $UP_KEY ]; then
move_up
elif [ $KEY = $DOWN_KEY ]; then
move_down
elif [ $KEY = $RIGHT_KEY ]; then
if [ $CURRENT = 3 ]; then
break
fi
fi
fi
done
# restaura a cor padrão do terminal
echo -e '\\e[0m'
# restaura as configurações de linha do terminal
stty $STTY_SETTINGS
Referências:
[1] http://en.wikipedia.org/wiki/Escape_sequence
[2] http://www.dicas-l.com.br/artigos/linux-modotexto/coluna11.html