Update debian files
[indicator-keyboard-led.git] / indicator-keyboard-led.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 #
4 # indicator-keyboard-led - simulate keyboard lock keys LED
5 # Copyright (c) 2017-2023 Adrian I Lam <spam@adrianiainlam.tk> s/spam/me/
6 #
7 # Permission is hereby granted, free of charge, to any person obtaining a
8 # copy of this software and associated documentation files (the "Software"),
9 # to deal in the Software without restriction, including without limitation
10 # the rights to use, copy, modify, merge, publish, distribute, sublicense,
11 # and/or sell copies of the Software, and to permit persons to whom the
12 # Software is furnished to do so, subject to the following conditions:
13 #
14 # The above copyright notice and this permission notice shall be included in
15 # all copies or substantial portions of the Software.
16 #
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20 # THE AUTHOR OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23 # IN THE SOFTWARE.
24 #
25 # I would like to thank Tobias Schlitt <toby@php.net>, who wrote
26 # indicator-chars <https://github.com/tobyS/indicator-chars> which I used
27 # as a reference when writing this software.
28
29 import signal
30 import subprocess
31 import os
32 import argparse
33 import shutil
34 import gi
35 gi.require_version('Gdk', '3.0')
36 gi.require_version('Gtk', '3.0')
37 gi.require_version('AppIndicator3', '0.1')
38 from gi.repository import Gdk, Gtk, AppIndicator3
39
40 APP_NAME = 'indicator-keyboard-led'
41 APP_VERSION = '1.2'
42
43 ICON_FOLDER = '/usr/share/icons/hicolor/scalable/apps/'
44 import gettext
45 t = gettext.translation(APP_NAME, '/usr/share/locale')
46 _ = t.gettext
47
48 class IndicatorKeyboardLED:
49 def __init__(self, order='NCS', xdotool=None):
50 self.validate_order(order)
51
52 self.indicators = {}
53
54 count = 0
55 for i in order:
56 self.indicators[i] = AppIndicator3.Indicator.new(
57 '-'.join([APP_NAME, str(count), i]),
58 os.path.join(ICON_FOLDER, '-'.join([APP_NAME, i, 'off.svg'])),
59 AppIndicator3.IndicatorCategory.APPLICATION_STATUS)
60 self.indicators[i].set_status(AppIndicator3.IndicatorStatus.ACTIVE)
61
62 count += 1
63
64 keymap = Gdk.Keymap.get_default()
65 keymap.connect('state-changed', self.update_indicator, order)
66 self.update_indicator(keymap, order)
67 self.create_menu(xdotool, order)
68
69 def create_menu(self, xdotool, order):
70 xdotool = xdotool or shutil.which('xdotool')
71 def send_keypress(menuitem, keystroke):
72 subprocess.call([xdotool, 'key', keystroke])
73 def new_menu_item(itemtype):
74 if itemtype == 'N':
75 item = Gtk.MenuItem.new_with_label(_('Num Lock'))
76 item.connect('activate', send_keypress, 'Num_Lock')
77 elif itemtype == 'C':
78 item = Gtk.MenuItem.new_with_label(_('Caps Lock'))
79 item.connect('activate', send_keypress, 'Caps_Lock')
80 elif itemtype == 'S':
81 item = Gtk.MenuItem.new_with_label(_('Scroll Lock'))
82 item.connect('activate', send_keypress, 'Scroll_Lock')
83 else:
84 raise ValueError('Invalid itemtype')
85 return item
86
87 for i in order:
88 menu = Gtk.Menu()
89
90 if xdotool and os.access(xdotool, os.X_OK) and os.path.isfile(xdotool):
91 menu.append(new_menu_item(i))
92
93 quit_item = Gtk.MenuItem.new_with_label(_('Quit'))
94 menu.append(quit_item)
95 quit_item.connect('activate', Gtk.main_quit)
96
97 self.indicators[i].set_menu(menu)
98 menu.show_all()
99
100 def update_indicator(self, keymap, order):
101 for i in order:
102 if i == 'N':
103 state = keymap.get_num_lock_state()
104 elif i == 'C':
105 state = keymap.get_caps_lock_state()
106 elif i == 'S':
107 state = keymap.get_scroll_lock_state()
108 else:
109 raise ValueError('Invalid value in ORDER')
110
111 icon = os.path.join(ICON_FOLDER, '-'.join([APP_NAME, i, 'on.svg' if state else 'off.svg']))
112 self.indicators[i].set_icon(icon)
113
114
115 def validate_order(self, order):
116 order = order.upper()
117 for i in order:
118 if i not in ['N', 'C', 'S']:
119 raise ValueError('Illegal character in ORDER. '
120 '(Choices: [N, C, S])')
121 if len(order) != len(set(order)):
122 raise ValueError('Repeated character in ORDER. '
123 'Please specify each lock at most once.')
124 if 'S' in order and Gtk.check_version(3, 18, 0) is not None:
125 # Silently drop Scroll lock if GTK <= 3.18
126 order.remove('S')
127
128
129 if __name__ == '__main__':
130 signal.signal(signal.SIGINT, lambda signum, frame: Gtk.main_quit())
131
132 parser = argparse.ArgumentParser(
133 description='indicator-keyboard-led - simulate keyboard lock keys LED',
134 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
135 parser.add_argument('-o', '--order', required=False, default='NCS',
136 help='specify the order of the locks displayed, e.g. CSN for '
137 'Caps Scroll Num, or NC for Num Caps without Scroll lock')
138 parser.add_argument('-x', '--xdotool', required=False,
139 help='provide full path to xdotool executable, '
140 'e.g. "/usr/bin/xdotool"; '
141 'if not specified, search in PATH')
142 args = parser.parse_args()
143
144 IndicatorKeyboardLED(order=args.order,
145 xdotool=args.xdotool)
146 Gtk.main()