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.