Pruebas con Gext

viernes, 11 de diciembre de 2009 0 comentarios
¡Hola!

He avanzado algo en la librería Gext, sobre todo en la parte relacionada con el módulo Mouse, así que me gustaría poner un vídeo muy sencillo de Gext en acción (y de paso pruebo las posibilidades para empotrar vídeo aquí, con Flowplayer, GPL v3.0).

El guión es muy sencillo, simplemente crea una carpeta en el escritorio, la mueve de sitio y la borra. Para ello, ha sido necesario usar los movimientos relativos y absolutos, los clicks (izquierdo y derecho) y el Drag & Drop. El script en python (mouse.py) se encuentra en la carpeta de ejemplos del repositorio. El vídeo lo he grabado con gtk-recordmydesktop, habilitando la opción --no-frame para poder usar el DnD.



Aquí os dejo. ¡Saludos!

python-xlib: emulando el teclado

martes, 1 de diciembre de 2009 2 comentarios
Salut!

En el pasado post nos quedamos a medias. Vimos como hacer algunas cosillas con el ratón, pero no hicimos nada con el teclado. Intentémoslo ahora.

Con la misma función, fake_input(), es posible enviar eventos del teclado. Cambiamos el ButtonPress/ButtonRelease por KeyPress/KeyRelease, y como parámetro, el código de la tecla que queremos. He te aquí el problema. ¿Qué es ese "código" y de donde lo sacamos?

Sería interesante disponer de documentación sobre esto, pero esta es bastante escasa. Así que nos toca ver el código fuente o los ejemplos que encontremos. Tampoco es que haya muchos... pero algunos si que hay. En mi búsqueda me encontré con xautomation, una herramienta de la que puedo sacar muchas cosas :p

Viendo su código, se puede deducir que a la función fake_input() es necesario pasarle un keycode, que a su vez, se obtiene a partir de un keysym. Dos conceptos nuevos que tendremos que definir. Al parecer, los keycodes están en un rango bastante reducido (hay menos de 255), por lo que deduzco que tienen una correspondencia (casi) directa con una tecla del teclado (esto, posiblemente no sea así, pero vamos a asumir que sí). Hay un método de la clase Display que hace la conversión entre un keysym y un keycode. Y en el módulo XK hay un montón de keysyms definidos (por ejemplo, XK_colon, XK_plus o XK_space).

Deduzco que, por un lado tenemos un número asociado al caracter que queremos pulsar, y por otro, la tecla a la que se corresponde con la distribución del teclado actual (Spain, USA, etc). Por ejemplo, si quiero introducir el caracter '.', primero tengo que identificar su keysym. Al parecer, es el 46, o XK_period (y además, se corresponde con el valor ASCII para el punto). Ahora, tenemos que traducir este keysym al valor de la tecla correspondiente, usando Display.keysym_to_keycode(). Y el valor retornado es el que podemos usar en fake_input().

Supongo que todo este jaleo es conveniente para dar soporte a múltiples distribuciones de teclados, así como a diferentes juegos de caracteres. Veamos un ejemplo, el clásico 'Hello World!', que tiene un poco de todo.

from Xlib.display import Display
from Xlib.ext import xtest
from Xlib import X, XK

d = Display()
msg = "Hello World!"

for c in msg:
    keysym = XK.string_to_keysym(c)
    keycode = d.keysym_to_keycode(keysym)

    xtest.fake_input(d, X.KeyPress, keycode)
    xtest.fake_input(d, X.KeyRelease, keycode)

d.flush()

He usado una función de XK que hace la traducción desde una cadena a un keysym (por ejemplo, convierte la cadena "space" al keysym XK_space). Lo probamos pero no muestra el resultado esperado, sino: 'helloworld' ¿Qué ha pasado con el espacio, las mayúsculas y el símbolo de admiración? Bien, vayamos por partes. Es normal que no se muestren las mayúsculas porque realmente no son la pulsación de una tecla, sino dos (el 'Bloq Mayus' se puede considerar como el uso repetido de 'Shift'). Tendremos que tenerlo en cuenta. Lo mismo pasa con la admiración. El caso del espacio es diferente. La función string_to_keysym() sólo reconoce las cadenas que tengan una correspondencia directa con algún atributo del módulo XK. Es decir, lo que necesitamos es el nombre del keysym sin el prefijo XK_. En este caso, habrá que "traducir" ese espacio, ' ', a la cadena 'space'. Haciendo los cambios oportunos, tendríamos:

from Xlib.display import Display
from Xlib.ext import xtest
from Xlib import X, XK

d = Display()
msg = "Hello World!"
shift = d.keysym_to_keycode(XK.XK_Shift_L)

for c in msg:
    if   c == ' ': c = "space"
    elif c == '!': c = "exclam"
    keysym = XK.string_to_keysym(c)
    keycode = d.keysym_to_keycode(keysym)

    if c in ["H","W", "exclam"]:
        xtest.fake_input(d, X.KeyPress, shift)

    xtest.fake_input(d, X.KeyPress, keycode)
    xtest.fake_input(d, X.KeyRelease, keycode)

    if c in ["H","W", "exclam"]:
        xtest.fake_input(d, X.KeyRelease, shift)

    d.flush()

Se nos ha complicado un poco el código. Como veis, la estrategia es pulsar la tecla SHIFT antes de cada caracter 'especial'. Además, es necesario convertir los símbolos a nombres válidos para XK. Dentro de gext, haremos todas estas conversiones, para que introducir cualquier texto sea lo más sencillo posible.

Espero no haberte aburrido mucho ;) Nos vemos en la siguiente entrada.
Happy hacking!

Au revoir!

Gext: python xlib

jueves, 26 de noviembre de 2009 0 comentarios
Nĭmen hăo!

Para llevar a cabo lo descrito en el post anterior, he pensado en utilizar la implementación de Xlib en Python, llamada python-xlib. Aunque no tiene un desarrollo continuado, creo que me servirá para lo que quiero hacer. El hecho de que esté escrita íntegramente en Python me supone una ventaja, para cotillear su código ;) Además, tiene también varias extensiones portadas. En concreto, la que me interesa es XTEST, que tiene algunos mecanismos para manipular la entrada.

Vamos a hacer algunas pruebas. Para seguirme, tendrás que tener instalado el paquete python-xlib. Esto depende de tu distribución. Por ejemplo, para Debian (y derivados como Ubuntu), el paquete está en los repositorios oficiales, con lo que un simple
# apt-get install python-xlib
lo soluciona.
Empecemos por lo fácil, vamos a intentar mover el ratón... sin tocarlo :D . Abre una consola Python (a mi me gusta ipython), o escríbelo en un fichero y lo ejecutas, como quieras. Primero pongo el código, y luego vemos que hace.

from Xlib.display import Display

d = Display()
r = d.screen().root
for i in range(200):
    r.warp_pointer(i, i)
    d.sync()

Sencillo, ¿verdad? Desgranemoslo. En primer lugar, importar la clase Display. Un display para Xlib es una estación de trabajo, es decir, un monitor, un teclado y un ratón (esto es muy simplista, porque puede haber muchas combinaciones diferentes, por ejemplo, dos o más monitores, o una tableta digitalizadora, etc.). Tenemos en cuenta que X es un servidor, y nosotros actuamos como clientes. Al crear un objeto Display, realmente lo que hacemos es conectarnos al servidor.

De este servidor, obtenemos el screen, un objeto complejo del que nos interesa principamente la ventana raíz, root. A esta ventana le podemos decir que mueva el ratón, con warp_pointer(). Hasta que no hacemos un sync() en el Display, no se envían las acciones al servidor, y por tanto es como si no hiciéramos nada. Para mejorar el efecto, en vez de movernos a un punto, lo que hacemos es desplazarnos por una recta. Para eso es el bucle.

Si lo ejecutas, deberías ver que el ratón se mueve a la esquina superior izquierda (el punto (0,0) de la ventana raíz), y se desplaza 200 píxels en los ejes x e y, con pasos de 1 pixel.

Hacer click es un poco más complejo. Para ello, necesitamos la extensión XTEST. Usaremos una función llamada fake_input(), que simula la pulsación de una tecla, tanto del ratón como del teclado. Veamos el código para pulsar el botón derecho del ratón:

from Xlib.ext import xtest
from Xlib import X

xtest.fake_input(d, X.ButtonPress, 3)
xtest.fake_input(d, X.ButtonRelease, 3)
d.sync()

Importamos los módulos correspondientes: xtest y X (para los eventos que generaremos). Como veis, se usa el mismo display, 'd', que habíamos creado antes (la misma conexión). Le indicamos qué evento queremos que mande al servidor, en este caso primero un ButtonPress y luego un ButtonRelease, y los datos relacionados con el evento, que aquí es el número del botón que queremos pulsar. Al hacer de nuevo un sync(), lo envía todo al servidor X, y dependiendo de donde esté el puntero del ratón, deberíamos ver un menú contextual, o algo así :D

¿Queréis probar el teclado? Bueno, pues eso creo que será en otro post, porque es un poco lioso :P .

Zàijiàn!

Sobre Gext (la librería)

miércoles, 25 de noviembre de 2009 0 comentarios
¡Hola!

Como veis, últimamente estoy más animado para avanzar con el proyecto, aunque sólo sea pensar (porque la fase de desarrollo todavía no está oficialmente abierta). Este post va sobre la librería que comentábamos en el anterior.

Me he dado cuenta de que tanto el editor como el compilador son sólo accesorios. Lo único imprescindible es la librería. Me explico: el objetivo final será un script, generado con el compilador a partir de un proyecto creado con el editor. Pues bien, ese script se puede escribir a mano (obviamente, no quiero que eso sea siempre así, pero para probar la librería es válido). Por lo que la parte por la que empezaré será esa: gext.

Según lo tengo pensado, una de las funciones principales será la de emular el comportamiento humano, es decir, generar eventos del teclado y del ratón. Actualmente, la mayoría de los sistemas a los que va dirigida esta aplicación sólo cuenta con un dispositivo de cada tipo (si esto cambia, ya veremos :D). Por ello, intuitivamente, tendremos dos objetos, uno para el teclado y otro para el ratón (sendos singletones ;) ). Estos objetos tendrán una interfaz que permita controlar los dispositivos programáticamente. Por ejemplo, para el ratón, se debería poder mover, pulsar el botón derecho, soltarlo (con soporte para todos los botones posibles, claro :D), obtener su posición, etc. Y para el teclado idem.

Me atrevería a esbozar las interfaces aquí mismo. Veamos. La del ratón:
class Mouse:
    def getPosition(self):
        pass
 
    def moveTo(self, x, y, abs=True):
        pass

    def pressButton(self, button):
        pass

    def releaseButton(self, button):
        pass

Las argumentos x e y son enteros que indican la posición, y abs especifica si el movimiento es absoluto (desde la posición 0,0 de la ventana root), o relativo a la posición actual del ratón. Button es un entero que indica el número de botón a pulsar. Se pueden añadir algunos métodos para simplificar ciertas tareas comunes, por ejemplo, un leftClick(), o un dragTo(). La del teclado, podría ser algo así:
class Keyboard:
    def pressKey(self, key):
        pass

    def releaseKey(self, key):
        pass
De nuevo, también añadiré algunos métodos para tareas comunes. En este caso, es muy frecuente querer introducir muchos caracteres seguidos, en vez de ir uno por uno. Para ello, algo como generateKeyEvents(seq) sería útil, donde seq es una cadena de texto.

Bueno, creo que por hoy, ya tengo bastante :D . ¡Hasta el siguiente post!