LockKeys 0.2

J'ai évoqué récemment mon besoin d'avoir une applet indicateur d'état du clavier (capslock et numlock) indépendante de l'environnement de bureau. Ne trouvant rien qui me satisfasse j'ai alors bidouillé un script en bash, mais avec dans un coin de ma tête l'idée que l'occasion était bien belle de me mettre enfin à Python. D'autant plus que j'utilise Battery Monitor (aka batterymon-clone) écrit en python2 et dont je me suis bien entendu largement inspiré.

Fonctionnement : une simple icône dans le systray avec un menu accessible par un clic droit

En option il est possible de jouer un son et/ou d'afficher une notification. Le jeux d'icônes et le fichier son doivent être installés pour l'instant dans /usr/local/share/lockkeys. Ils sont réunis dans cette archive.

Voici le code

#!/usr/bin/python2
# -*- coding: utf-8 -*-

#Todo mettre les messages sous forme de variable dans fichier à inclure <- internationalisation

import gtk
import glib
import os
import sys
import ConfigParser
import ctypes
try:
    import pynotify
    if not pynotify.init("LockKeys"):
        print("Il y a eu une erreur pendant l'initialisation du système de notification. Les notifications ne fonctionnerons pas.")
        pynotify = None
except:
    print("Il semble que python-notify ne soit pas installé. Les notifications ne fonctionnerons pas.")
    pynotify = None
NotifyAvailable = pynotify
 
class XKeyboardState(ctypes.Structure):
    _fields_ = [("key_click_percent", ctypes.c_int),
                ("bell_percent", ctypes.c_int),
                ("bell_pitch", ctypes.c_uint),
                ("bell_duration", ctypes.c_uint),
                ("led_mask", ctypes.c_ulong),
                ("global_auto_repeat", ctypes.c_int),
                ("auto_repeats", ctypes.c_char * 32)]
 
def initXGetKeyboardControl():
    global dpy, keyboardState, XGetKeyboardControl
   
    libX11 = ctypes.CDLL("libX11.so.6")
    XOpenDisplay = libX11.XOpenDisplay
    XOpenDisplay.restype = ctypes.c_void_p
    XOpenDisplay.argtypes = [ctypes.c_char_p]
    XGetKeyboardControl = libX11.XGetKeyboardControl
    XGetKeyboardControl.restype = ctypes.c_int
    XGetKeyboardControl.argtypes = [ctypes.c_void_p, ctypes.POINTER(XKeyboardState)]
   
    dpy = XOpenDisplay(None)
    keyboardState = XKeyboardState()
 
def runXGetKeyboardControl():
    global dpy, keyboardState, XGetKeyboardControl
    XGetKeyboardControl(dpy, ctypes.byref(keyboardState))
    return keyboardState.led_mask
 
#écrit dans le fichier de config    
def WriteConfig (Section, Key, Value):
    Config.set(Section,Key,Value)
    with open(ConfigFile, 'w') as myfile:
        Config.write(myfile)
 
#Notification (état du clavier) si pynotify != None
def notify(message,duration):
    if pynotify:
        n = pynotify.Notification('LockKeys', message)
        n.set_timeout(duration)
        n.set_icon_from_pixbuf(gtk.Label().render_icon(gtk.STOCK_DIALOG_INFO, gtk.ICON_SIZE_LARGE_TOOLBAR))
        n.show()
 
 
class Systray():
    def __init__(self):
        self.tray_object= gtk.StatusIcon()
        self.tray_object.connect("popup_menu", self.rightclick_menu)
        self.show_trayicon(1) ## fixed to one for now
        self._oldMask = -1 #int(runXGetKeyboardControl()) & 3
 
    def show_trayicon(self,value):
       self.tray_object.set_visible(True)
       return
 
    def property_modified(self):
        # utilse runXGetKeyboardControl() pour connaître l'état des touches capslock et numlock
        # 0 -> aucun, 1 -> capslock, 2-> numlock, 3 -> les 2
        mask=int(runXGetKeyboardControl()) & 3
        if mask != self._oldMask:
            if sound_status == True and self._oldMask != -1:
                os.system(Sound)
            notify(msg[mask],2000)
            self._oldMask = mask
            # Todo : essayer d'abord usr/share/lockkeys/ voire ~/.local/lockkeys
            icon_path = '/usr/local/share/lockkeys/' + str(mask) + '.png'
            self.tray_object.set_from_file(icon_path)
 
    # défini le menu clic droit sur l'icône
    def rightclick_menu(self, button, widget, event):
        menu = gtk.Menu()
        about_menu = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
        about_menu.connect('activate', self.about)
        exit_menu = gtk.ImageMenuItem(gtk.STOCK_CLOSE)
        exit_menu.connect('activate', self.close)
        menu.append(about_menu)
        menu.append(exit_menu)
        sep = gtk.SeparatorMenuItem()
        menu.append(sep)
        sound_menu = gtk.CheckMenuItem("Activer le son")
        sound_menu.set_active(sound_status)
        sound_menu.connect("activate", self.sound_toggle)
        menu.append(sound_menu)
        notify_menu = gtk.CheckMenuItem("Activer les notifications")
        notify_menu.set_active(notify_status)
        notify_menu.connect("activate", self.notify_toggle)
        menu.append(notify_menu)
        menu.show_all()
        menu.popup(None, None, None, 2, event)
       
    # activation / désactivation du son et enregistrement dans le fichier config
    def sound_toggle(self, widget):
        global sound_status
        if widget.active:
            sound_status=True
        else:
            sound_status=False
        WriteConfig ('helpers','sound',sound_status)
 
    # activation / désactivation des notifications et enregistrement dans le fichier config
    def notify_toggle(self, widget):
        global notify_status
        global pynotify
        if widget.active:
            notify_status=True
            pynotify=NotifyAvailable
        else:
            notify_status=False
            pynotify=None
        WriteConfig ('helpers','notification',notify_status)
 
    def close(self,button):
        sys.exit(0)
 
    def about(self, button):
        about_dg = gtk.AboutDialog()
        about_dg.set_name('Lockkeys')
        about_dg.set_version('0.2')
        about_dg.set_copyright('(C) 2014 Vincent Gay <vgay@vintherine.org>')
        about_dg.set_comments(("Simple icône dans la zone de notification pour indiquer l'état de CapsLock et NumLock"))
        about_dg.set_license('Ce script est distribuable sous licence gpl version 3 ou supérieure\\nhttp://www.gnu.org/licenses/gpl-3.0.fr.html')
        about_dg.set_website('http://blog.vintherine.org')
        about_dg.run()
        about_dg.destroy()
 
class Manager:
    def __init__(self):
        self.listener = Systray()
       
    def __property_modified_handler(self):
        self.listener.property_modified()
 
    def update(self):
        self.__property_modified_handler()
        return True
 
def main():
    initXGetKeyboardControl()
    m = Manager()
    glib.timeout_add(200, m.update)
    gtk.main()
 
ConfigFile=os.path.expanduser('~/.config/lockkeys.cfg')
# aplay appartient au paquet alsa, est-ce la peine de vérifier ?
Sound = 'aplay /usr/local/share/lockkeys/ding.wav > /dev/null 2>1&'
sound_status=True
pynotify = None
Config = ConfigParser.ConfigParser()
msg=[]
msg.append('Capslock = off, Numlock = off')
msg.append('Capslock = on, Numlock = off')
msg.append('Capslock = off, Numlock = on')
msg.append('Capslock = on, Numlock = on')
 
#créer le fichier de config s'il n'existe pas
if os.path.isfile(ConfigFile) == False:
    ini = open(ConfigFile,'w')
    Config.add_section('helpers')
    Config.set('helpers','sound',True)
    Config.set('helpers','notification',False)
    Config.write(ini)
    ini.close()
 
# lire le fichier de configuration    
Config.read(ConfigFile)
try:
    sound_status = Config.getboolean("helpers", "sound")
except:
    WriteConfig ('helpers','sound',True)
try:
    notify_status = Config.getboolean("helpers", "notification")
except:
    WriteConfig ('helpers','notification',False)
    notify_status=False
if notify_status:
    pynotify=NotifyAvailable
   
main()

Si l'indentation n'est pas propre suite au copié collé merci de prendre le code sur pastebin

Dépendances :

  • python2
  • gtk2
  • pygtk
  • python2-notify (optionnel)
  • aplay (installé par alsa donc en principe présent)

Il me reste toutefois un point me chiffonne avec cette solution : le timeout qui gère l'appel à gtk. S'il est trop haut il y a un décalage entre l'appui sur les touches et le changement d'icône. S'il est trop bas la charge cpu, quoique supportable, excède 1%. Ce qui me paraît beaucoup pour une simple applet. Avec un compromis à 400 ms la charge ressort à 0,7% (Intel Pentium 2020M 2.4Ghz double cœur).

edit : problème résolu grâce aux conseils de Benjarobin. Le code ci-dessus a été modifié en conséquence.

Voilou, Il reste encore du boulot pour rendre l'application présentable mais c'est mon premier script python : ça s'arrose :-)

Chez moi ça fonctionne correctement mais si quelqu'un d'autre voulait bien tester ça serait sympa.

Vus : 1156
Publié par Vincent Gay : 38