1#!/usr/bin/env python3

2#

3# This file is part of Checkbox.

4#

5# Copyright 2012 Canonical Ltd.

6#

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.

10

11#

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.

16#

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/>.

19

20

21 import gi

22 import os

23 import sys

24

25 import fcntl

26 import gettext

27 import struct

28 import termios

29

30 from gettext import gettext as _

31

32 from gi.repository import GLib

33 from optparse import OptionParser

34

35

36 EXIT_WITH_FAILURE = 1

37 EXIT_WITH_SUCCESS = 0

38 EXIT_TIMEOUT = 30

39

40# Keyboard options from /usr/include/linux/kd.h

41 K_RAW = 0x00

42 K_XLATE = 0x01

43 K_MEDIUMRAW = 0x02

44 K_UNICODE = 0x03

45 K_OFF = 0x04

46 KDGKBMODE = 0x4B44

47 KDSKBMODE = 0x4B45

48

49

50 def ioctl_p_int(fd, request, value=0):

51 s = struct.pack("i", value)

52 s2 = fcntl.ioctl(fd, request, s)

53 (ret,) = struct.unpack("i", s2) # This always returns a tuple.

54 return ret

55

56

57 class Key:

58

59 def __init__(self, codes, name=None):

60 self.codes = codes

61 self.name = name

62 self.tested = False

63 self.required = True

64

65 @property

66 def status(self):

67 if not self.required:

68 return _("Not required")

69 if not self.tested:

70 return _("Untested")

71 return _("Tested")

72

73

74 class Reporter(object):

75

76 exit_code = EXIT_WITH_FAILURE

77

78 def __init__(self, main_loop, keys, scancodes=False):

79 self.main_loop = main_loop

80 self.keys = keys

81 self.scancodes = scancodes

82

83 self.fileno = os.open("/dev/console", os.O_RDONLY)

84 GLib.io_add_watch(self.fileno, GLib.IO_IN, self.on_key)

85

86 # Set terminal attributes

87 self.saved_attributes = termios.tcgetattr(self.fileno)

88 attributes = termios.tcgetattr(self.fileno)

89 attributes[3] &= ~(termios.ICANON | termios.ECHO)

90 attributes[6][termios.VMIN] = 1

91 attributes[6][termios.VTIME] = 0

92 termios.tcsetattr(self.fileno, termios.TCSANOW, attributes)

93

94 # Set keyboard mode

95 self.saved_mode = ioctl_p_int(self.fileno, KDGKBMODE)

96 mode = K_RAW if scancodes else K_MEDIUMRAW

97 fcntl.ioctl(self.fileno, KDSKBMODE, mode)

98

99 def _parse_codes(self, raw_bytes):

100 """Parse the given string of bytes to scancodes or keycodes."""

101 if self.scancodes:

102 return self._parse_scancodes(raw_bytes)

103 else:

104 return self._parse_keycodes(raw_bytes)

105

106 def _parse_scancodes(self, raw_bytes):

107 """Parse the bytes in raw_bytes into a scancode."""

108 index = 0

109 length = len(raw_bytes)

110 while index < length:

111 if index + 1 < length and raw_bytes[index] == 0xE0:

112 code = (raw_bytes[index] << 8) | raw_bytes[index + 1]

113 index += 2

114 else:

115 code = raw_bytes[0]

116 index += 1

117

118 yield code

119

120 def _parse_keycodes(self, raw_bytes):

121 """Parse the bytes in raw_bytes into a keycode."""

122 index = 0

123 length = len(raw_bytes)

124 while index < length:

125 if (

126 index + 2 < length

127 and (raw_bytes[index] & 0x7F) == 0

128 and (raw_bytes[index + 1] & 0x80) != 0

129 and (raw_bytes[index + 2] & 0x80) != 0

130 ):

131 code = ((raw_bytes[index + 1] & 0x7F) << 7) | (

132 raw_bytes[2] & 0x7F

133 )

134 index += 3

135 else:

136 code = raw_bytes[0] & 0x7F

137 index += 1

138

139 yield code

140

141 @property

142 def required_keys_tested(self):

143 """Returns True if all keys marked as required have been tested"""

144 return all([key.tested for key in self.keys if key.required])

145

146 def show_text(self, string):

147 pass

148

149 def quit(self, exit_code=EXIT_WITH_FAILURE):

150 self.exit_code = exit_code

151

152 termios.tcsetattr(self.fileno, termios.TCSANOW, self.saved_attributes)

153 fcntl.ioctl(self.fileno, KDSKBMODE, self.saved_mode)

154

155 # FIXME: Having a reference to the mainloop is suboptimal.

156 self.main_loop.quit()

157

158 def found_key(self, key):

159 key.tested = True

160

161 def toggle_key(self, key):

162 key.required = not key.required

163 key.tested = False

164

165 def on_key(self, source, cb_condition):

166 raw_bytes = os.read(source, 18)

167 for code in self._parse_codes(raw_bytes):

168 if code == 1:

169 # Check for ESC key pressed

170 self.show_text(_("Test cancelled"))

171 self.quit()

172 elif 1 < code < 10 and isinstance(self, CLIReporter):

173 # Check for number to skip

174 self.toggle_key(self.keys[code - 2])

175 else:

176 # Check for other key pressed

177 for key in self.keys:

178 if code in key.codes:

179 self.found_key(key)

180 break

181

182 return True

183

184

185 class CLIReporter(Reporter):

186

187 def __init__(self, *args, **kwargs):

188 super(CLIReporter, self).__init__(*args, **kwargs)

189

190 self.show_text(_("Please press each key on your keyboard."))

191 self.show_text(

192 _("I will exit automatically once all keys " "have been pressed.")

193 )

194 self.show_text(

195 _(

196 "If your keyboard lacks one or more keys, "

197 "press its number to skip testing that key."

198 )

199 )

200 self.show_text(_("You can also close me by pressing ESC or Ctrl+C."))

201

202 self.show_keys()

203

204 def show_text(self, string):

205 sys.stdout.write(string + "\n")

206 sys.stdout.flush()

207

208 def show_keys(self):

209 self.show_text("---")

210 for index, key in enumerate(self.keys):

211 self.show_text(

212 "%(number)d - %(key)s - %(status)s"

213 % {"number": index + 1, "key": key.name, "status": key.status}

214 )

215

216 def found_key(self, key):

217 super(CLIReporter, self).found_key(key)

218 self.show_text(

219 _("%(key_name)s key has been pressed" % {"key_name": key.name})

220 )

221

222 self.show_keys()

223 if self.required_keys_tested:

224 self.show_text(_("All required keys have been tested!"))

225 self.quit(EXIT_WITH_SUCCESS)

226

227 def toggle_key(self, key):

228 super(CLIReporter, self).toggle_key(key)

229 self.show_keys()

230

231

232 class GtkReporter(Reporter):

233

234 def __init__(self, *args, **kwargs):

235 super(GtkReporter, self).__init__(*args, **kwargs)

236

237 gi.require_version("Gdk", "3.0")

238 gi.require_version("Gtk", "3.0")

239 from gi.repository import Gdk, Gtk

240

241 # Initialize GTK constants

242 self.ICON_SIZE = Gtk.IconSize.BUTTON

243 self.ICON_TESTED = Gtk.STOCK_YES

244 self.ICON_UNTESTED = Gtk.STOCK_INDEX

245 self.ICON_NOT_REQUIRED = Gtk.STOCK_REMOVE

246

247 self.button_factory = Gtk.Button

248 self.hbox_factory = Gtk.HBox

249 self.image_factory = Gtk.Image

250 self.label_factory = Gtk.Label

251 self.vbox_factory = Gtk.VBox

252

253 # Create GTK window.

254 window = Gtk.Window()

255 window.set_type_hint(Gdk.WindowType.TOPLEVEL)

256 window.set_size_request(100, 100)

257 window.set_resizable(False)

258 window.set_title(_("Key test"))

259 window.connect("delete_event", lambda w, e: self.quit())

260 window.connect(

261 "key-release-event",

262 lambda w, k: k.keyval == Gdk.KEY_Escape and self.quit(),

263 )

264 window.show()

265

266 # Add common widgets to the window.

267 vbox = self._add_vbox(window)

268 self.label = self._add_label(vbox)

269 button_hbox = self._add_hbox(vbox)

270 validation_hbox = self._add_hbox(vbox)

271 skip_hbox = self._add_hbox(vbox)

272 exit_button = self._add_button(vbox, _("_Exit"), True)

273 exit_button.connect("clicked", lambda w: self.quit())

274

275 # Add widgets for each key.

276 self.icons = {}

277 for key in self.keys:

278 stock = getattr(Gtk, "STOCK_MEDIA_%s" % key.name.upper(), None)

279 if stock:

280 self._add_image(button_hbox, stock)

281 else:

282 self._add_label(button_hbox, key.name)

283 self.icons[key] = self._add_image(validation_hbox, Gtk.STOCK_INDEX)

284 button = self._add_button(skip_hbox, _("Skip"))

285 button.connect("clicked", self.on_skip, key)

286

287 self.show_text(_("Please press each key on your keyboard."))

288 self.show_text(

289 _(

290 "If a key is not present in your keyboard, "

291 "press the 'Skip' button below it to remove it "

292 "from the test."

293 )

294 )

295

296 def _add_button(self, context, label, use_underline=False):

297 button = self.button_factory(label=label, use_underline=use_underline)

298 context.add(button)

299 button.show()

300 return button

301

302 def _add_hbox(self, context, spacing=4):

303 hbox = self.hbox_factory()

304 context.add(hbox)

305 hbox.set_spacing(4)

306 hbox.show()

307 return hbox

308

309 def _add_image(self, context, stock):

310 image = self.image_factory(stock=stock, icon_size=self.ICON_SIZE)

311 context.add(image)

312 image.show()

313 return image

314

315 def _add_label(self, context, text=None):

316 label = self.label_factory()

317 context.add(label)

318 label.set_size_request(0, 0)

319 label.set_line_wrap(True)

320 if text:

321 label.set_text(text)

322 label.show()

323 return label

324

325 def _add_vbox(self, context):

326 vbox = self.vbox_factory()

327 vbox.set_homogeneous(False)

328 vbox.set_spacing(8)

329 context.add(vbox)

330 vbox.show()

331 return vbox

332

333 def show_text(self, string):

334 self.label.set_text(self.label.get_text() + "\n" + string)

335

336 def check_keys(self):

337 if self.required_keys_tested:

338 self.show_text(_("All required keys have been tested!"))

339 self.quit(EXIT_WITH_SUCCESS)

340

341 def found_key(self, key):

342 super(GtkReporter, self).found_key(key)

343 self.icons[key].set_from_icon_name(

344 self.ICON_TESTED, size=self.ICON_SIZE

345 )

346

347 self.check_keys()

348

349 def on_skip(self, sender, key):

350 self.toggle_key(key)

351 if key.required:

352 stock_icon = self.ICON_UNTESTED

353 else:

354 stock_icon = self.ICON_NOT_REQUIRED

355 self.icons[key].set_from_icon_name(stock_icon, self.ICON_SIZE)

356

357 self.check_keys()

358

359

360 def main(args):

361 gettext.textdomain("checkbox")

362

363 usage = """\

364 Usage: %prog [OPTIONS] CODE...

365

366 Syntax for codes:

367

368 57435 - Decimal code without name

369 0160133:Super - Octal code with name

370 0xe05b,0xe0db:Super - Multiple hex codes with name

371

372 Hint to find codes:

373

374 The showkey command can show keycodes and scancodes.

375"""

376 parser = OptionParser(usage=usage)

377 parser.add_option(

378 "-i",

379 "--interface",

380 default="auto",

381 help="Interface to use: cli, gtk or auto",

382 )

383 parser.add_option(

384 "-s",

385 "--scancodes",

386 default=False,

387 action="store_true",

388 help="Test for scancodes instead of keycodes.",

389 )

390 (options, args) = parser.parse_args(args)

391

392 # Get reporter factory from options or environment.

393 if options.interface == "auto":

394 if "DISPLAY" in os.environ:

395 reporter_factory = GtkReporter

396 else:

397 reporter_factory = CLIReporter

398 elif options.interface == "cli":

399 reporter_factory = CLIReporter

400 elif options.interface == "gtk":

401 reporter_factory = GtkReporter

402 else:

403 parser.error("Unsupported interface: %s" % options.interface)

404

405 if not args:

406 parser.error("Must specify codes to test.")

407

408 # Get keys from command line arguments.

409 keys = []

410 for codes_name in args:

411 if ":" in codes_name:

412 codes, name = codes_name.split(":", 1)

413 else:

414 codes, name = codes_name, codes_name

415

416 # Guess the proper base from the string.

417 codes = [int(code, 0) for code in codes.split(",")]

418 key = Key(codes, name)

419 keys.append(key)

420

421 main_loop = GLib.MainLoop()

422 try:

423 reporter = reporter_factory(main_loop, keys, options.scancodes)

424 except OSError:

425 parser.error("Failed to initialize interface: %s" % options.interface)

426

427 try:

428 main_loop.run()

429 except KeyboardInterrupt:

430 reporter.show_text(_("Test interrupted"))

431 reporter.quit()

432

433 return reporter.exit_code

434

435

436 if __name__ == "__main__":

437 sys.exit(main(sys.argv[1:]))