Edit code and rename files to prepare for .deb packaging
[indicator-keyboard-led.git] / indicator-keyboard-led.py
index 44e02a0..9213a83 100755 (executable)
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 #
 # indicator-keyboard-led - simulate keyboard lock keys LED
-# Copyright (c) 2016 Adrian I Lam <adrianiainlam@gmail.com>
+# Copyright (c) 2017 Adrian I Lam <me@adrianiainlam.tk>
 #
 # Permission is hereby granted, free of charge, to any person obtaining a
 # copy of this software and associated documentation files (the "Software"),
 
 import signal
 import subprocess
-from os import path
+import os
 import argparse
+import shutil
 import gi
 gi.require_version('Gdk', '3.0')
 gi.require_version('Gtk', '3.0')
 gi.require_version('AppIndicator3', '0.1')
 from gi.repository import Gdk, Gtk, AppIndicator3
 
+APP_NAME = 'indicator-keyboard-led'
+APP_VERSION = '1.1'
+
+ICON_LOCATION = '/usr/share/icons/hicolor/scalable/apps/' + APP_NAME + '.svg'
+import gettext
+t = gettext.translation(APP_NAME, '/usr/share/locale')
+_ = t.gettext
+
 class IndicatorKeyboardLED:
-    locks = { 'caps': 'Caps', 'num': 'Num', 'scr': 'Scroll' }
-    
-    def __init__(self, short=False):
+    def __init__(self, short=False, order='NCS', xdotool=None):
+        self.validate_order(order)
+
         self.indicator = AppIndicator3.Indicator.new(
-            'indicator-keyboard-led',
-            path.join(path.dirname(path.realpath(__file__)), 'icon.svg'),
+            APP_NAME, ICON_LOCATION,
             AppIndicator3.IndicatorCategory.APPLICATION_STATUS)
         self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
-        
+
         if short:
-            self.locks = { 'caps': 'C', 'num': 'N', 'scr': 'S' }
+            self.locks = { 'N': _('N'), 'C': _('C'), 'S': _('S') }
+        else:
+            self.locks = { 'N': _('Num'), 'C': _('Caps'), 'S': _('Scroll') }
 
         keymap = Gdk.Keymap.get_default()
-        keymap.connect('state-changed', self.update_indicator)
-        self.update_indicator(keymap)
+        keymap.connect('state-changed', self.update_indicator, order)
+        self.update_indicator(keymap, order)
+        self.create_menu(xdotool, order)
 
+    def create_menu(self, xdotool, order):
         menu = Gtk.Menu()
-        items = {
-            'caps' : Gtk.MenuItem.new_with_label('Caps Lock'),
-            'num'  : Gtk.MenuItem.new_with_label('Num Lock'),
-            'scr'  : Gtk.MenuItem.new_with_label('Scroll Lock')
-        }
-        menu.append(items['caps'])
-        menu.append(items['num'])
-        menu.append(items['scr'])
-
-        items['caps'].connect('activate', self.send_keypress, 'Caps_Lock')
-        items['num' ].connect('activate', self.send_keypress, 'Num_Lock')
-        items['scr' ].connect('activate', self.send_keypress, 'Scroll_Lock')
+        xdotool = xdotool or shutil.which('xdotool')
+        if xdotool and os.access(xdotool, os.X_OK) and os.path.isfile(xdotool):
+            def send_keypress(menuitem, keystroke):
+                subprocess.call([xdotool, 'key', keystroke])
+            def new_menu_item(itemtype):
+                if itemtype == 'N':
+                    item = Gtk.MenuItem.new_with_label(_('Num Lock'))
+                    item.connect('activate', send_keypress, 'Num_Lock')
+                elif itemtype == 'C':
+                    item = Gtk.MenuItem.new_with_label(_('Caps Lock'))
+                    item.connect('activate', send_keypress, 'Caps_Lock')
+                elif itemtype == 'S':
+                    item = Gtk.MenuItem.new_with_label(_('Scroll Lock'))
+                    item.connect('activate', send_keypress, 'Scroll_Lock')
+                else:
+                    raise ValueError('Invalid itemtype')
+                return item
+
+            for i in order:
+                menu.append(new_menu_item(i))
+            menu.append(Gtk.SeparatorMenuItem())
+
+        quit_item = Gtk.MenuItem.new_with_label(_('Quit'))
+        menu.append(quit_item)
+        quit_item.connect('activate', Gtk.main_quit)
 
         self.indicator.set_menu(menu)
         menu.show_all()
 
-    def update_indicator(self, keymap):
-        label = '⚫' if keymap.get_caps_lock_state() else '⚪'
-        label += self.locks['caps'] + ' '
-        label += '⚫' if keymap.get_num_lock_state() else '⚪'
-        label += self.locks['num'] + ' '
-        label += '⚫' if keymap.get_scroll_lock_state() else '⚪'
-        label += self.locks['scr']
-        self.indicator.set_label(label, '')
+    def update_indicator(self, keymap, order):
+        labels = []
+        for i in order:
+            if i == 'N':
+                state = keymap.get_num_lock_state()
+            elif i == 'C':
+                state = keymap.get_caps_lock_state()
+            elif i == 'S':
+                state = keymap.get_scroll_lock_state()
+            else:
+                raise ValueError('Invalid value in ORDER')
+            labels += [('⚫' if state else '⚪') + self.locks[i]]
+        self.indicator.set_label(' '.join(labels), '')
+
+    def validate_order(self, order):
+        order = order.upper()
+        for i in order:
+            if i not in ['N', 'C', 'S']:
+                raise ValueError('Illegal character in ORDER. '
+                                 '(Choices: [N, C, S])')
+        if len(order) != len(set(order)):
+            raise ValueError('Repeated character in ORDER. '
+                             'Please specify each lock at most once.')
+        if 'S' in order and Gtk.check_version(3, 18, 0) is not None:
+            # Silently drop Scroll lock if GTK <= 3.18
+            order.remove('S')
 
-    def send_keypress(self, menuitem, keystroke):
-        subprocess.call(['xdotool', 'key', keystroke])
 
 if __name__ == '__main__':
     signal.signal(signal.SIGINT, lambda signum, frame: Gtk.main_quit())
-    
+
     parser = argparse.ArgumentParser(
-        description='indicator-keyboard-led - simulate keyboard lock keys LED')
-    parser.add_argument('-s', '--short', dest='short', action='store_true',
-        help='use short label, i.e. ⚫C ⚫N ⚫S instead of ⚫Caps ⚫Num ⚫Scroll',
-        required=False)
+        description='indicator-keyboard-led - simulate keyboard lock keys LED',
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+    parser.add_argument('-s', '--short', required=False, action='store_true',
+        help='use short label, i.e. ⚫N ⚫C ⚫S instead of ⚫Num ⚫Caps ⚫Scroll')
+    parser.add_argument('-o', '--order', required=False, default='NCS',
+        help='specify the order of the locks displayed, e.g. CSN for '
+             '⚫Caps ⚫Scroll ⚫Num, or NC for ⚫Num ⚫Caps without Scroll lock')
+    parser.add_argument('-x', '--xdotool', required=False,
+        help='provide full path to xdotool executable, '
+             'e.g. "/usr/bin/xdotool"; '
+             'if not specified, search in PATH')
     args = parser.parse_args()
-    
-    IndicatorKeyboardLED(short=args.short)
+
+    IndicatorKeyboardLED(short=args.short, order=args.order,
+                         xdotool=args.xdotool)
     Gtk.main()