3
# This file is part of Checkbox.
5
# Copyright 2012 Canonical Ltd.
7
# Checkbox is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License version 3,
9
# as published by the Free Software Foundation.
12
# Checkbox is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
29
from gettext import gettext as _
30
from gi.repository import GObject
31
from optparse import OptionParser
38
# Keyboard options from /usr/include/linux/kd.h
48
def ioctl_p_int(fd, request, value=0):
49
s = struct.pack("i", value)
50
s2 = fcntl.ioctl(fd, request, s)
51
(ret,) = struct.unpack("i", s2) # This always returns a tuple.
57
def __init__(self, codes, name=None):
66
return _("Not required")
72
class Reporter(object):
74
exit_code = EXIT_WITH_FAILURE
76
def __init__(self, main_loop, keys, scancodes=False):
77
self.main_loop = main_loop
79
self.scancodes = scancodes
81
self.fileno = os.open("/dev/console", os.O_RDONLY)
82
GObject.io_add_watch(self.fileno, GObject.IO_IN, self.on_key)
84
# Set terminal attributes
85
self.saved_attributes = termios.tcgetattr(self.fileno)
86
attributes = termios.tcgetattr(self.fileno)
87
attributes[3] &= ~(termios.ICANON | termios.ECHO)
88
attributes[6][termios.VMIN] = 1
89
attributes[6][termios.VTIME] = 0
90
termios.tcsetattr(self.fileno, termios.TCSANOW, attributes)
93
self.saved_mode = ioctl_p_int(self.fileno, KDGKBMODE)
94
mode = K_RAW if scancodes else K_MEDIUMRAW
95
fcntl.ioctl(self.fileno, KDSKBMODE, mode)
97
def _parse_codes(self, raw_bytes):
98
"""Parse the given string of bytes to scancodes or keycodes."""
100
return self._parse_scancodes(raw_bytes)
102
return self._parse_keycodes(raw_bytes)
104
def _parse_scancodes(self, raw_bytes):
105
"""Parse the bytes in raw_bytes into a scancode."""
107
length = len(raw_bytes)
108
while index < length:
109
if (index + 1 < length and raw_bytes[index] == 0xE0):
110
code = ((raw_bytes[index] << 8) | raw_bytes[index + 1])
118
def _parse_keycodes(self, raw_bytes):
119
"""Parse the bytes in raw_bytes into a keycode."""
121
length = len(raw_bytes)
122
while index < length:
123
if (index + 2 < length and (raw_bytes[index] & 0x7f) == 0
124
and (raw_bytes[index + 1] & 0x80) != 0
125
and (raw_bytes[index + 2] & 0x80) != 0):
126
code = (((raw_bytes[index + 1] & 0x7f) << 7) |
127
(raw_bytes[2] & 0x7f))
130
code = (raw_bytes[0] & 0x7f)
136
def required_keys_tested(self):
137
"""Returns True if all keys marked as required have been tested"""
138
return all([key.tested for key in self.keys if key.required])
140
def show_text(self, string):
143
def quit(self, exit_code=EXIT_WITH_FAILURE):
144
self.exit_code = exit_code
146
termios.tcsetattr(self.fileno, termios.TCSANOW, self.saved_attributes)
147
fcntl.ioctl(self.fileno, KDSKBMODE, self.saved_mode)
149
# FIXME: Having a reference to the mainloop is suboptimal.
150
self.main_loop.quit()
152
def found_key(self, key):
155
def toggle_key(self, key):
156
key.required = not key.required
159
def on_key(self, source, cb_condition):
160
raw_bytes = os.read(source, 18)
161
for code in self._parse_codes(raw_bytes):
163
# Check for ESC key pressed
164
self.show_text(_("Test cancelled"))
166
elif 1 < code < 10 and type(self) == CLIReporter:
167
# Check for number to skip
168
self.toggle_key(self.keys[code - 2])
170
# Check for other key pressed
171
for key in self.keys:
172
if code in key.codes:
179
class CLIReporter(Reporter):
181
def __init__(self, *args, **kwargs):
182
super(CLIReporter, self).__init__(*args, **kwargs)
184
self.show_text(_("Please press each key on your keyboard."))
185
self.show_text(_("I will exit automatically once all keys "
186
"have been pressed."))
187
self.show_text(_("If your keyboard lacks one or more keys, "
188
"press its number to skip testing that key."))
189
self.show_text(_("You can also close me by pressing ESC or Ctrl+C."))
193
def show_text(self, string):
194
sys.stdout.write(string + "\n")
198
self.show_text("---")
199
for index, key in enumerate(self.keys):
201
"%(number)d - %(key)s - %(status)s" %
202
{"number": index + 1, "key": key.name, "status": key.status})
204
def found_key(self, key):
205
super(CLIReporter, self).found_key(key)
207
_("%(key_name)s key has been pressed" % {'key_name': key.name}))
210
if self.required_keys_tested:
211
self.show_text(_("All required keys have been tested!"))
212
self.quit(EXIT_WITH_SUCCESS)
214
def toggle_key(self, key):
215
super(CLIReporter, self).toggle_key(key)
219
class GtkReporter(Reporter):
221
def __init__(self, *args, **kwargs):
222
super(GtkReporter, self).__init__(*args, **kwargs)
224
from gi.repository import Gdk, Gtk
226
# Initialize GTK constants
227
self.ICON_SIZE = Gtk.IconSize.BUTTON
228
self.ICON_TESTED = Gtk.STOCK_YES
229
self.ICON_UNTESTED = Gtk.STOCK_INDEX
230
self.ICON_NOT_REQUIRED = Gtk.STOCK_REMOVE
232
self.button_factory = Gtk.Button
233
self.hbox_factory = Gtk.HBox
234
self.image_factory = Gtk.Image
235
self.label_factory = Gtk.Label
236
self.vbox_factory = Gtk.VBox
239
window = Gtk.Window()
240
window.set_type_hint(Gdk.WindowType.TOPLEVEL)
241
window.set_size_request(100, 100)
242
window.set_resizable(False)
243
window.set_title(_("Key test"))
244
window.connect("delete_event", lambda w, e: self.quit())
247
lambda w, k: k.keyval == Gdk.KEY_Escape and self.quit())
250
# Add common widgets to the window.
251
vbox = self._add_vbox(window)
252
self.label = self._add_label(vbox)
253
button_hbox = self._add_hbox(vbox)
254
validation_hbox = self._add_hbox(vbox)
255
skip_hbox = self._add_hbox(vbox)
256
exit_button = self._add_button(vbox, _("_Exit"), True)
257
exit_button.connect("clicked", lambda w: self.quit())
259
# Add widgets for each key.
261
for key in self.keys:
262
stock = getattr(Gtk, "STOCK_MEDIA_%s" % key.name.upper(), None)
264
self._add_image(button_hbox, stock)
266
self._add_label(button_hbox, key.name)
267
self.icons[key] = self._add_image(validation_hbox, Gtk.STOCK_INDEX)
268
button = self._add_button(skip_hbox, _("Skip"))
269
button.connect("clicked", self.on_skip, key)
271
self.show_text(_("Please press each key on your keyboard."))
272
self.show_text(_("If a key is not present in your keyboard, "
273
"press the 'Skip' button below it to remove it "
276
def _add_button(self, context, label, use_underline=False):
277
button = self.button_factory(label=label, use_underline=use_underline)
282
def _add_hbox(self, context, spacing=4):
283
hbox = self.hbox_factory()
289
def _add_image(self, context, stock):
290
image = self.image_factory(stock=stock, icon_size=self.ICON_SIZE)
295
def _add_label(self, context, text=None):
296
label = self.label_factory()
298
label.set_size_request(0, 0)
299
label.set_line_wrap(True)
305
def _add_vbox(self, context):
306
vbox = self.vbox_factory()
307
vbox.set_homogeneous(False)
313
def show_text(self, string):
314
self.label.set_text(self.label.get_text() + "\n" + string)
316
def check_keys(self):
317
if self.required_keys_tested:
318
self.show_text(_("All required keys have been tested!"))
319
self.quit(EXIT_WITH_SUCCESS)
321
def found_key(self, key):
322
super(GtkReporter, self).found_key(key)
323
self.icons[key].set_from_stock(self.ICON_TESTED, size=self.ICON_SIZE)
327
def on_skip(self, sender, key):
330
stock_icon = self.ICON_UNTESTED
332
stock_icon = self.ICON_NOT_REQUIRED
333
self.icons[key].set_from_stock(stock_icon, self.ICON_SIZE)
339
gettext.textdomain("checkbox")
342
Usage: %prog [OPTIONS] CODE...
346
57435 - Decimal code without name
347
0160133:Super - Octal code with name
348
0xe05b,0xe0db:Super - Multiple hex codes with name
352
The showkey command can show keycodes and scancodes.
354
parser = OptionParser(usage=usage)
355
parser.add_option("-i", "--interface",
357
help="Interface to use: cli, gtk or auto")
358
parser.add_option("-s", "--scancodes",
361
help="Test for scancodes instead of keycodes.")
362
(options, args) = parser.parse_args(args)
364
# Get reporter factory from options or environment.
365
if options.interface == "auto":
366
if "DISPLAY" in os.environ:
367
reporter_factory = GtkReporter
369
reporter_factory = CLIReporter
370
elif options.interface == "cli":
371
reporter_factory = CLIReporter
372
elif options.interface == "gtk":
373
reporter_factory = GtkReporter
375
parser.error("Unsupported interface: %s" % options.interface)
378
parser.error("Must specify codes to test.")
380
# Get keys from command line arguments.
382
for codes_name in args:
383
if ":" in codes_name:
384
codes, name = codes_name.split(":", 1)
386
codes, name = codes_name, codes_name
388
# Guess the proper base from the string.
389
codes = [int(code, 0) for code in codes.split(",")]
390
key = Key(codes, name)
393
main_loop = GObject.MainLoop()
395
reporter = reporter_factory(main_loop, keys, options.scancodes)
397
parser.error("Failed to initialize interface: %s" % options.interface)
401
except KeyboardInterrupt:
402
reporter.show_text(_("Test interrupted"))
405
return reporter.exit_code
407
if __name__ == "__main__":
408
sys.exit(main(sys.argv[1:]))