Ações acionadoras do teclado e do mouse com voz e o xdotool

Obtenha pressionamentos de tecla ao combinar entrada de voz com a biblioteca xdotool

A xdotool é uma biblioteca útil de instruções que permite que os programadores emulem o pressionamento de teclas e as ações do mouse. A força específica da ferramenta entra em cena quando o teclado ou o mouse está ausente ou em situações de acessibilidade em que o usuário não é fisicamente capaz de usar os métodos regulares de entrada. Esse artigo tem dois objetivos — primeiro, fornecer uma introdução ao uso da xdotool em um ambiente de desktop do Linux® e, segundo, usar entrada de voz para disparar ações tipicamente realizadas por meio de entrada de hardware. Um exemplo definitivo usa XML para armazenar fragmentos de código orientados a xdotool para inserção em código de gerenciador de diálogo gerado automaticamente.

Colin Beckingham, Writer and Researcher, Freelance

Colin Beckingham é pesquisador freelancer, escritor e programador. Ele mora na região leste de Ontário, no Canadá. Com diplomas da Universidade de Queen, em Kingston, e da Universidade de Windsor, atuou em uma grande variedade de áreas, incluindo o setor financeiro, horticultura, corrida de cavalos, ensino, serviço público e viagens e turismo. Autor de aplicativos de banco de dados, inúmeros jornais, revistas e artigos on-line. Seu interesse em pesquisa inclui programação de software livre, VoIP e aplicativos por controle de voz no Linux. É possível entrar em contato com Colin através do e-mail colbec@start.ca.



22/Set/2011

Alguns de nós já estamos acostumados com a entrada por teclado e mouse: você digita um caractere no teclado e ele aparece na tela, ou digita uma cadeia de caractere, pressiona Return e alguma ação ocorre — seja localmente ou em rede. O que mais há a se esperar? Mas, e se você não tiver ou não puder usar um teclado ou um mouse, ou se quiser pressionar uma tecla em uma janela para que ela execute algo em uma janela diferente em um desktop diferente? Ou, talvez, você queira criar uma janela, redimensioná-la, abrir um navegador naquela janela, navegar para uma URL e, em seguida, usar a tecla tab para percorrer uma série de links na página da Web e clicar em um deles — tudo isso sem um teclado ou mouse, usando sua voz com um reconhecedor de voz. Essa abordagem requer a emulação do teclado e do mouse.

Acrônimos usados frequentemente

  • CDATA: Dados de caracteres
  • URL: Interface com o usuário
  • XML: linguagem de marcação extensível

O acesso a chamadas de sistema que realizam todas essas tarefas é perfeitamente possível, mas talvez inconveniente, pois envolve o uso de um nível inferior de codificação do que a maioria dos desenvolvedores normalmente usa. Assim como um reconhecedor de voz facilita o acesso a padrões de voz gravados, outras bibliotecas dão acesso a esquemas de janelas em desktops Linux. Insira xdotool.

xdotool

A xdotool (consulte Recursos para obter um link) é uma biblioteca de funções que o ajuda a enviar instruções para o ambiente de janelas. Ela pode enviar uma cadeia de caractere a ser digitada para uma janela remota, redimensionar a janela, enviar pressionamentos de teclas individuais e combinados ou até mesmo realizar um clique duplo do botão direito do mouse. E o aplicativo remoto não sabe que isto está sendo manipulado, ou voxipulado, indiretamente. O Web site tem instruções específicas para a instalação usando make.

Enviar comandos da xdotool é direto: a sintaxe é uma xdotool simples seguida por opções e argumentos.

xdotool search --name 'mytest'

Nesse exemplo, xdotool busca janelas com o título de mytest e retorna seus números de ID, que podem ser gravados para uso posterior. Tenha cuidado com a pesquisa de janelas sem algum tipo de critério — você acabará com uma longa lista de números sem significado.

Você pode obter dicas sobre os nomes para usar em teclas e combinações de pressionamento de teclas do Web site. Uma pequena armadilha é que, apesar de tab parecer intuitivo, somente Tab funciona em meu sistema.

Apesar de ser possível emitir comandos da xdotool, um de cada vez, a partir de uma janela do terminal, algumas variáveis internas são perdidas cada vez que a instância de xdotool termina, portanto, encadear uma série de opções e argumentos depois de uma única chamada de xdotool é mais eficiente.

Provavelmente a melhor forma de entender isso é vendo um exemplo funcional.


Automatize um console

A Listagem 1 mostra um teste que usa uma janela de terminal (chame-a de work) para abrir outro terminal (mytest) e enviar pressionamentos de teclas para ela. Os comandos estão em um script somente com PHP para fornecer uma maneira de desacelerar o processo para que você possa ver o que acontece: é possível usar Perl, Python ou seu mecanismo favorito de script. O efeito do script é definir uma série de pressionamentos de teclas, fazer com que o terminal funcional mostre "Trying A" (em que A é uma tecla) e enviar essa tecla para o segundo terminal. Você deverá ver atividade em ambos os terminais ao mesmo tempo. Se o código executar muito rapidamente para que seja visto, aumente a constante definida WAIT.

Listagem 1. Terminais duplicados
<?php
// script to test xdotool functionality
define('WAIT',200000);
$charrarray = setca();
$execcmd = "xdotool search --name 'mytest'";
exec($execcmd,$out);
$windowid = $out[0];
if ($windowid) {
  echo "Found window ID ".$windowid."\n";
} else {
  exec('konsole -p LocalTabTitleFormat=mytest');
  $execcmd = "xdotool search --name 'mytest'";
  exec($execcmd,$out);
  $windowid = $out[0];
  // die("Cannot find the window\n");
}
// now activate the window
$execcmd = "xdotool windowactivate --sync $windowid";
exec($execcmd,$out);
// test keys
foreach ($charrarray as $k=>$char) {
  echo "Trying $char \n";
  send_key($k);
}
// functions
function send_string($string) {
  $execcmd = "xdotool type $string";
  exec($execcmd,$out);
  usleep(WAIT);
}
function send_key($key) {
  $execcmd = "xdotool key $key";
  exec($execcmd,$out);
  usleep(WAIT);
}
function send_return() {
  $execcmd = "xdotool key Return";
  exec($execcmd,$out);
  usleep(WAIT);
}
function setca() {
$ca = array(
   "Up" => 'up',
   "Down" => 'down',
   "ampersand" => '&',
   "apostrophe" => '\'',
   "Left" => 'L',
   "Right" => 'R',
   "asciicircum" => '^',
   "asciitilde" => '~', 
   "asterisk" => '*',   
   "at" => '@',
   "backslash" => '\\',
   "bar" => '|',
   "braceleft" => '{',
   "braceright" => '}',
   "bracketleft" => '[',
   "bracketright" => ']',
   "colon" => ':',
   "comma" => ',',
   "dollar" => '$',
   "equal" => '=', 
   "exclam" => '!',
   "grave" => '`',
   "greater" => '>',
   "less" => '<',
   "minus" => '-',
   "numbersign" => '#',
   "parenleft" => '(',
   "parenright" => ')',
   "percent" => '%',
   "period" => '.',    
   "plus" => '+',      
   "question" => '?',  
   "quotedbl" => '"',  
   "semicolon" => ';', 
   "space" => ' ',
   "BackSpace" => 'bs',
   "Tab" => '\t', 
   "underscore" => '_',
   "slash" => '/', 
   "eacute" => 'eac', 
   "ccedilla" => 'cced', 
   "udiaeresis" => 'uum', 
   "idiaeresis" => 'ïdi', 
   "Home" => '<',
   "End" => '>',
   "Return" => '\n'
);
return $ca;
}
?>

Esse script começa definindo uma constante WAIT que tem escopo global. Essa constante desacelera as funções para que você possa ver o que está acontecendo. A seguir, ele preenche um array com os pressionamentos de teclas que você deseja testar. A lista completa, é claro, é muito maior: essa amostra contém muitos dos caracteres padrões, além de algumas possibilidades mais incomuns. A xdotool, a seguir, procura uma janela com o título mytest; se não encontrar uma, o PHP a cria emitindo uma instrução konsole com um parâmetro que dá à janela um título específico:

exec('konsole -p LocalTabTitleFormat=mytest');

Depois de criar a nova janela, a pesquisa da xdotool é repetida e uma variável global é definida com o número de identidade daquela janela. Isso garante que você esteja enviando os comandos subsequentes para o destino certo. Com a janela disponível, o script a ativa e começa a percorrer o array de caracteres de teste em um loop. Para cada um, ele imprime no console de trabalho que está tentando um caractere específico e, a seguir, imprime aquele caractere na janela de destino.

Quando tiver certeza de que tem a combinação certa de argumentos e opções da xdotool , poderá ganhar o benefício da brevidade usando as capacidades de encadeamento e script da xdotool. A estrutura PHP é útil quando você não tem certeza do que usar e precisa ter um controle de diagnóstico completo.


Automatizar um navegador de texto

Apesar de a Listagem 1 ser produtiva no sentido de que permite testar os nomes das teclas, ela seria ainda mais produtiva para recuperar informações da Internet. O exemplo na Listagem 2 abre uma nova janela, inicia uma instância do navegador de texto Lynx e navega para o Web site www.ibm.com.

Listagem 2. Automatizar o Lynx
<?php
// script to test xdotool functionality
// first get the window to launch lynx
define('WAIT',200000);
$winname = 'mylynx';
$execcmd = "xdotool search --name $winname";
exec($execcmd,$out);
$windowid = $out[0];
if ($windowid) {
  echo "Found window ID ".$windowid."\n";
} else {
  exec("konsole -p LocalTabTitleFormat=$winname");
  exec($execcmd,$out);
  $windowid = $out[0];
  // die("Cannot find the window\n");
}
// now activate the window
$execcmd = "xdotool windowactivate --sync $windowid";
exec($execcmd,$out);
$execcmd = "xdotool getactivewindow windowsize --sync 750 750";
exec($execcmd,$out);
// start sending stuff
send_string("lynx");
send_return(); // opens lynx browser
send_key("g"); // get URL prompt
send_string("www.ibm.com"); 
send_return(); // send a URL
function send_string($string) {
  $execcmd = "xdotool type $string";
  exec($execcmd,$out);
  usleep(WAIT);
}
function send_key($key) {
  $execcmd = "xdotool key $key";
  exec($execcmd,$out);
  usleep(WAIT);
}
function send_return() {
  $execcmd = "xdotool key Return";
  exec($execcmd,$out);
  usleep(WAIT);
}
?>

O código na Listagem 2 é similar ao código na Listagem 1 no sentido de que define o período de espera para desacelerar as coisas um pouco e, a seguir, procura uma janela chamada mylynx (para não confundir com mytest acima), salva o ID da janela para uso posterior, ativa a janela, a redimensiona e inicia uma instância do Lynx na janela. A seguir, o script envia um pressionamento de tecla g para aquela janela, que diz ao Lynx para esperar um URL, e segue-o com a cadeia de caractere www.ibm.com e a tecla Return. O Web site agora está aberta e pronta para receber comandos adicionais, como Tab e Return para navegação.

Porque escolher o Lynx como um aplicativo a automatizar? Você poderá descobrir que, ao tentar automatizar outros navegadores ou aplicativos, alguns pressionamentos de teclas automatizados são ignorados. A raiz desse problema está no medo de cross-site scripting. O Lynx parece permitir um pouco mais de pressionamentos de teclas falsos do que, digamos, o Mozilla Firefox. Sua experiência poderá variar.


Automatizar com voz

Quando tudo estiver funcionando e você vir como é fácil automatizar usando pressionamentos de teclas indiretos, surge a pergunta sobre entrada a partir de outros dispositivos. Voz é uma das opções. Um reconhecedor de voz usando um modelo de áudio adequadamente treinado pode receber instruções verbais e usar a xdotool para executar comandos. Se estiver procurando um aplicativo pronto que empregue xdotool e o modelo de áudio VoxForge, confira kiku (consulte Recursos para obter links com mais informações), que está disponível para o Ubuntu.


xdotool e o gerenciador de diálogo

O código de exemplo na Listagem 3 mostra como usar xdotool em um gerenciador de diálogo.

Listagem 3. xdotool no gerenciador de diálogo
<?php
...
function process($major,$minor) {
global $globalcontext;
  switch ($major) {
    case 'CONTEXT':
      switch ($minor) {
	case 'SOFTWARE':
	  $globalcontext = $minor;
	  echo "Set global context to software\n";
	break;
	default:
	  echo "Defaulted out $minor\n";
	break;
      }
    break;
    case 'BROWSER':
      $ooc = ($globalcontext == 'SOFTWARE') ? true : false ;
      if ($ooc) {
	switch ($minor) {
	  case 'OPEN':
	    echo "Open browser\n";
	    browser_open();
	  break;
	  case 'CLOSE':
	    echo "Close browser\n";
	    browser_close();
	  break;
	  case 'LOCATION':
	    echo "Go to URL\n";
	    browser_location();
	  break;
	  default:
	    echo "Defaulted out $minor\n";
	  break;
	}
      } else {
	// OOC
	echo "Recognized but out of context\n";
      }
    break;
    default:
	  echo "Defaulted out $major\n";
    break;
  }
}
function browser_open() {
global $windowid;
  $winname = 'mylynx';
  $execcmd = "xdotool search --name $winname";
  exec($execcmd,$out);
  $windowid = $out[0];
  if ($windowid) {
    echo "Found window ID ".$windowid."\n";
  } else {
    exec("konsole -p LocalTabTitleFormat=$winname");
    $execcmd = "xdotool search --name $winname";
    exec($execcmd,$out);
    $windowid = $out[0];
    echo "Using windowid $windowid";
    // die("Cannot find the window\n");
  }
  // now activate the window
  $execcmd = "xdotool windowactivate --sync $windowid";
  exec($execcmd,$out);
  $execcmd = "xdotool getactivewindow windowsize --sync 800 800";
  exec($execcmd,$out);
  // start sending stuff
  send_string("lynx");
  send_return(); // opens lynx browser
}
...
  ?>

A Listagem 3 mostra duas funções. A primeira é process(), que espera dois argumentos. Esses argumentos são cadeias de caracteres que o processo de reconhecimento de voz retorna. Para saber mais sobre o processo de obtenção dessas cadeias de caracteres, consulte os tutoriais do VoxForge ou Sphinx (consulte Recursos para obter os links). Nesse caso, as cadeias de caracteres esperadas vêm de uma gramática que, em parte, consiste em instruções CONTEXT SOFTWARE e BROWSER OPEN.

Seguindo o processo, considere o que acontece quando o reconhecedor de voz escuta CONTEXT SOFTWARE. A cadeia de caractere principal é CONTEXT e a menor é SOFTWARE. A função do processo declara acesso a uma variável global na qual o contexto é armazenado e, a seguir, no switch define a variável de contexto para SOFTWARE. Essa variável é conhecida globalmente, portanto, posteriormente, quando o reconhecedor de voz detectar BROWSER OPEN principal e menor, o comutador pode manipular isto, mas primeiro verifica se o contexto está correto. A ideia em usar um contexto é ajudar a eliminar resultados incorretos do reconhecedor. Se seu contexto for SOCCER e você não acha que seja relevante abrir um navegador naquele contexto, ele não será aberto.

Se o contexto for correto, então o comando BROWSER OPEN prossegue para a função browser_open(), que você reconhecerá rapidamente como sendo, basicamente, o mesmo código usado na Listagem 2.

Quanto mais complexo se tornar seu gerenciamento de contexto, mais será necessário um método para desdobrar instruções comutadas que seguem regras definidas em outro local que reproduzam fielmente sua estrutura de gerenciamento de contexto. É possível definir essas regras de várias formas. Uma forma é usar a estrutura da Especificação de Gramática de Reconhecimento de Voz e a Interpretação Semântica para Reconhecimento de Voz — consulte Recursos para obter links para mais informações. E uma abordagem ligeiramente mais simples é armazenar os fragmentos de código necessários, incluindo as instruções xdotool, em uma estrutura XML.


Use XML para armazenar contexto e fragmentos de código

A Listagem 4 é um arquivo XML que tem detalhes sobre os blocos de construção de um gerenciador de diálogo imaginário. A vantagem de um arquivo XLM simples e editável é que todos os seus contextos e funções são vistos no mesmo arquivo; como eles estão separados do código comutado mais complexo do gerenciador de diálogo, é mais fácil ver e editar a estrutura do contexto. Esses dados contêm uma instrução xdotool para realizar um clique do botão esquerdo do mouse em que o cursor do mouse estiver no momento. Esse comando é válido em todos os contextos e, portanto, não tem um atributo ctxt.

Listagem 4. O armazenamento em XML
<?xml version="1.0" encoding="UTF-8"?>
<snips>
  <context>
    <func>click_left</func> 
    <func ctxt="software">browser_open</func>    
    <func ctxt="software">browser_location</func>    
    <func ctxt="software">browser_close</func>    
    <func ctxt="hardware">cpu_temperature</func>    
    <func ctxt="hardware">fan_speed</func>    
  </context>
  <snip fn="click_left">
<![CDATA[
function click_left() {
  exec('xdotool click 1');
}
]]>
  </snip>
  <snip fn="cpu_temperature">
<![CDATA[
function cpu_temperature() {
  $g = 0;
}
]]>
  </snip>
  <snip fn="fan_speed">
<![CDATA[
function fan_speed() {
  $g = 1;
}
]]>
  </snip>
  <snip fn="browser_open">
<![CDATA[
function browser_open() {
  $f = 0;
}
]]>
  </snip>
  <snip fn="browser_location">
<![CDATA[
function browser_location() {
  $f = 1;
}
]]>
</snip>
  <snip fn="browser_close">
<![CDATA[
function browser_close() {
  $f = 2;
}
]]>
</snip>
</snips>

Nesse código, o elemento raiz é snips. Esse elemento tem dois tipos de filhos:

  • Um elemento de contexto com informação contextual
  • Uma série de elementos snip que contêm fragmentos de código

O elemento de contexto contém elementos func, cada um dos quais tem o nome de uma função como o valor do elemento e um atributo ctxt que contém o contexto no qual a função é válida. Portanto, a função fan_speed não seria válida se o contexto estivesse definido como software. A função click_left, no entanto, não tem contexto, portanto ela é válida em qualquer lugar. Os fragmentos de código são armazenados em segmentos CDATA, o que garante que o analisador de XML ignore essas seções e aceite-as, independentemente do código que elas contenham.

Agora, só é necessário que o script expanda os dados do diálogo em seu próprio script. O código PHP na Listagem 5 faz justamente isso.

Listagem 5. Gerador do gerenciador de diálogo
<?php
// pull DM structure from an xml file
$xml = simplexml_load_file('snipstor.xml');
// get the contexts
// generate the main switch
echo "function process(\$major,\$minor) {\n";
echo "global \$globalcontext;\n";
echo "  switch (\$major) {\n";
$majtmp = "";
$mintmp = "";
foreach ($xml->context->func as $mjmn) {
  list($major,$minor) = explode("_",$mjmn);
  //echo "$major,$minor\n";
  if ($major != $majtmp) {
    if ($majtmp != "") echo "      default:
      echo \"Failed \$minor\";
      break;
      }\n    } else {
      echo 'OOC';\n    }\n";
    echo "  case '$major':\n";
    if ($minor != $mintmp) {
      $test = ($mjmn['ctxt']) ? "\$globalcontext == '".$mjmn['ctxt']."'" : 'true' ;
      echo "    if ($test) {\n";
      echo "      switch (\$minor) {\n";
      $mintmp = $minor;
    }
    echo "      case '$minor':
        $mjmn();
      break;\n";
    $majtmp = $major;
  } else {
    echo "      case '$minor':
      break;\n";
  }
}
echo "      default:
      echo \"Failed \$minor\";
      break;
      }\n    } else {
      echo 'OOC';\n    }\n";
echo "  default:
    echo \"Failed \$major\";
  break;
  }\n";
echo "}\n";
// generate the code snippets
echo "// functions\n";
foreach ($xml->snip as $snipfn) {
  echo trim($snipfn);
  echo "\n";
}
?>

O código na Listagem 5 grava mais código PHP do que é possível colocar em um gerenciador de diálogo. Ele começa lendo a estrutura do gerenciador de diálogo da Listagem 4 gravada em um arquivo chamado snipstor.xml em uma variável SimpleXML. A seguir, ele lê o conteúdo dos nomes das funções no elemento de contexto, extrai os componentes principal e menor desses nomes e usa-os para criar as instruções de comutação que controlam o fluxo no gerenciador de diálogo. À medida que ele cria o código, o script procura um contexto (no atributo ctxt ) no qual o comando seja válido. Ele insere uma instrução if condicional para cuidar de quaisquer contextos que possam ser aplicados ao caso do comutador. Se houver um contexto, ele é inserido como uma expressão a ser avaliada em tempo de execução; caso contrário, simplesmente insere true para que o programa sempre execute instruções fechadas — ou seja, o comando é válido em todos os contextos. Finalmente, o script fornece a biblioteca de código que os casos do comutador precisam.

O resultado da execução da Listagem 5 nos dados contidos na Listagem 4 segue na Listagem 6.

Listagem 6 Extração resultante do gerenciador de diálogo
function process($major,$minor) {
global $globalcontext;
  switch ($major) {
  case 'click':
    if (true) {
      switch ($minor) {
      case 'left':
        click_left();
      break;
      default:
      echo "Failed $minor";
      break;
      }
    } else {
      echo 'OOC';
    }
  case 'browser':
    if ($globalcontext == 'software') {
      switch ($minor) {
      case 'open':
        browser_open();
      break;
      case 'location':
      break;
      case 'close':
      break;
      default:
      echo "Failed $minor";
      break;
      }
    } else {
      echo 'OOC';
    }
  case 'cpu':
    if ($globalcontext == 'hardware') {
      switch ($minor) {
      case 'temperature':
        cpu_temperature();
      break;
      default:
      echo "Failed $minor";
      break;
      }
    } else {
      echo 'OOC';
    }
  case 'fan':
    if ($globalcontext == 'hardware') {
      switch ($minor) {
      case 'speed':
        fan_speed();
      break;
      default:
      echo "Failed $minor";
      break;
      }
    } else {
      echo 'OOC';
    }
  default:
    echo "Failed $major";
  break;
  }
}
// functions
function click_left() {
  exec('xdotool click 1');
}
function cpu_temperature() {
  $g = 0;
}
function fan_speed() {
  $g = 1;
}
function browser_open() {
  $f = 0;
}
function browser_location() {
  $f = 1;
}
function browser_close() {
  $f = 2;
}

Essa saída é similar àquilo com que você começou na Listagem 3. Note que o conteúdo real de CDATA é um código válido, mas abreviado por questões de simplicidade. O OOC é uma abreviação para Out of Context. Essa mensagem é exibida quando o reconhecedor de voz escuta um enunciado que é válido, mas não faz sentido de acordo com a estrutura do arquivo snipstor.xml. Para tornar o exemplo mais significativo, é possível substituir a seção CDATA pela função browser_open() com o código para aquela função a partir da Listagem 3.


Conclusão

Como se pode ver, xdotool é uma biblioteca útil de chamadas para o sistema de janelas. Combinada com um reconhecedor de voz, é possível usar voz para iniciar comandos da xdotool sob o controle de um gerenciador de diálogo. E, finalmente, como o gerenciador de diálogo pode se tornar bastante complexo e ineficiente quando os contextos forem complicados, é possível usar fragmentos de códigos contendo suas instruções xdotool armazenadas convenientemente em seções XML CDATA para gerar o gerenciador de diálogo de forma consistente e efetiva.

Recursos

Aprender

Obter produtos e tecnologias

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Linux, Software livre
ArticleID=758680
ArticleTitle=Ações acionadoras do teclado e do mouse com voz e o xdotool
publish-date=09222011