blob: 9f47d78899540def1b2dc56a2b56bacc4ba0cb9d [file] [log] [blame]
#!/usr/bin/python -u
#
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Filters keys in X.
It is used to consume and discard shortcut keys listed in _GRAB_KEYS.
It has two classes: KeyFilterImpl and KeyFilter. KeyFilter is a launcher
that runs/spawns this executable as a standalone process. KeyFilterImpl
is the actual implementation of key filter.
Usage:
Standalone process: ./key_filter.py
Fork a subprocess in a Python code:
key_filter = KeyFilter()
key_filter.Start() # non-blocking
...
# when done...
key_filter.Stop()
"""
import argparse
import collections
import logging
import signal
# Guard loading Xlib because it is currently not available in the
# image build process host-depends list. Failure to load in
# production should always manifest during regular use.
try:
from Xlib import X, XK # pylint: disable=F0401
from Xlib.display import Display # pylint: disable=F0401
_has_Xlib = True
except: # pylint: disable=W0702
_has_Xlib = False
import factory_common # pylint: disable=W0611
from cros.factory.test import factory
from cros.factory.utils.process_utils import Spawn
from cros.factory.utils import sys_utils
Keystroke = collections.namedtuple('Keystroke', ['mod', 'key'])
def InitGrabKeys(grab_keys):
"""Initializes a list of keys to be grabbed.
Args:
keys: a list to store Keystroke to be grabbed.
"""
if not _has_Xlib:
logging.error('Python Xlib module does not exist.')
return
del grab_keys[:]
# Search, tab, and function keys (back, forward, reload, etc.)
# with any modifiers.
grab_keys.extend([
Keystroke(X.AnyModifier, k) for k in (
'Super_L Tab F1 F2 F3 F5 F6 F7 F8 F9 F10'.split())])
# Ctrl+?
grab_keys.extend([
Keystroke(X.ControlMask, k) for k in (
'N T O Q W T Tab P S R plus minus 0 D F G K E Return U M '
'H J question L 1 2 3 4 5 6 7 8 9'). split()])
# Ctrl+Shift+?
grab_keys.extend([
Keystroke(X.ControlMask | X.ShiftMask, k) for k in (
'N Q W T Tab L R D G I J B').split()])
# Alt+?
grab_keys.extend([
Keystroke(X.Mod1Mask, k) for k in (
'1 2 3 4 5 6 7 8 9 Tab Return E F D').split()])
# Alt+Shift+?
grab_keys.extend([
Keystroke(X.Mod1Mask | X.ShiftMask, k) for k in (
'Tab B T S').split()])
# Ctrl+Alt+?
grab_keys.extend([
Keystroke(X.ControlMask | X.Mod1Mask, 'question'),
Keystroke(X.ControlMask | X.Mod1Mask, 'Z')])
class KeyFilterImpl(object):
"""Filters specific keypress events.
It consumes and discards certain keypress events.
Args:
disp: X's Display() object.
unmap_caps_lock: True to unmap CapsLock key.
caps_lock_keycode: Keycode for CapsLock.
For some cases, we cannot get Caps_Lock keysyms value. So we ask for
CapsLock keycode value here.
TODO(deanliao): figure out why and fix it.
"""
def __init__(self, disp, unmap_caps_lock=False, caps_lock_keycode=0,
power_keycode=0):
self._disp = disp
self._disp.allow_events(X.AsyncKeyboard, X.CurrentTime)
self._root = self._disp.screen().root
self._root.change_attributes(event_mask=X.KeyPressMask)
self._unmap_caps_lock = unmap_caps_lock
self._grab_keys = []
InitGrabKeys(self._grab_keys)
if power_keycode:
self._grab_keys.append(Keystroke(X.AnyModifier, power_keycode))
if unmap_caps_lock:
self._caps_lock_keycode = caps_lock_keycode
if self._caps_lock_keycode:
self._caps_lock_keymapping = self._disp.get_keyboard_mapping(
self._caps_lock_keycode, 1)[0].tolist()
def StringToKeycode(self, s):
"""Looks up a keycode for a keystroke.
Args:
s: keystroke name, e.g. 'Caps_Lock'.
Returns:
keycode. 0 if no found.
"""
return self._disp.keysym_to_keycode(XK.string_to_keysym(s))
def GrabKey(self, mod, key):
"""Grabs keypress event.
Args:
mod: modifier
key: key (string. But can use integer to represent keycode)
"""
logging.debug('GrabKey(mod:%d, key:%s)', mod, key)
keycode = key if isinstance(key, int) else self.StringToKeycode(key)
if keycode:
self._root.grab_key(keycode, mod, 1, X.GrabModeAsync, X.GrabModeAsync)
else:
logging.error('Keycode not found for key: %s', key)
def UngrabKey(self, mod, key):
"""Ungrabs keypress event.
Args:
mod: modifier
key: key (string. But can use integer to represent keycode)
"""
logging.debug('UngrabKey(mod:%d, key:%s)', mod, key)
keycode = key if isinstance(key, int) else self.StringToKeycode(key)
if keycode:
self._root.ungrab_key(keycode, mod)
def Run(self):
"""Starts filtering keys.
It is a blocking call to set up keys to grab and to consume grabbed
keypress event.
"""
# A hack to disable CapsLock by clearing its key mapping.
if self._unmap_caps_lock and self._caps_lock_keycode:
self._disp.change_keyboard_mapping(
self._caps_lock_keycode, [(0,) * len(self._caps_lock_keymapping)])
for k in self._grab_keys:
self.GrabKey(k.mod, k.key)
try:
while True:
grabbed_event = self._disp.next_event()
logging.debug('grabbed_event: %s', str(grabbed_event))
except Exception as e:
logging.error('Unable to grab key event: %s', str(e))
def Terminate(self):
"""Stops filtering keys."""
for k in self._grab_keys:
self.UngrabKey(k.mod, k.key)
# Restore CapsLock's key mapping.
if self._unmap_caps_lock and self._caps_lock_keycode:
self._disp.change_keyboard_mapping(self._caps_lock_keycode,
self._caps_lock_keymapping)
class KeyFilter(object):
"""A launcher to run key filter in a sub-process.
Args:
unmap_caps_lock: True to unmap CapsLock key; default False.
caps_lock_keycode: (int) Keycode of CapsLock; default 0.
Usage:
key_filter = KeyFilter()
key_filter.Start() # non-blocking
...
# when done...
key_filter.Stop()
"""
def __init__(self, unmap_caps_lock=False, caps_lock_keycode=0):
self._unmap_caps_lock = unmap_caps_lock
self._caps_lock_keycode = caps_lock_keycode
self._process = None
def Start(self):
cmd = [__file__.replace('.pyc', '.py')]
if self._unmap_caps_lock:
cmd.append('--unmap_caps_lock')
if self._caps_lock_keycode:
cmd.extend(['--caps_lock_keycode', str(self._caps_lock_keycode)])
logging.debug('Spawn: %s', ' '.join(cmd))
try:
if not sys_utils.InChroot():
self._process = Spawn(cmd)
except Exception as e:
logging.error('Error running Spawn("%s"): %s', ' '.join(cmd), str(e))
def Stop(self):
if self._process:
self._process.kill()
self._process.wait()
def main():
parser = argparse.ArgumentParser(
description='Filter ChromeOS shortcut keys.')
parser.add_argument('--verbose', '-v',
action='store_true', help='Enable debug logging.')
parser.add_argument('--unmap_caps_lock', action='store_true',
help='Unmap CapsLock key.')
parser.add_argument('--caps_lock_keycode', type=int, default=66,
help='CapsLock keycode; default 66.')
# Not every platform defines keysym for power button. Let's assign keycode
# here.
parser.add_argument('--power_keycode', type=int, default=124,
help='Power button keycode; default 124.')
args = parser.parse_args()
factory.init_logging('key_filter', verbose=args.verbose)
if not _has_Xlib:
logging.warning('Python Xlib module does not exist, probably on Freon.')
exit(1)
key_filter_impl = KeyFilterImpl(Display(),
unmap_caps_lock=args.unmap_caps_lock,
caps_lock_keycode=args.caps_lock_keycode,
power_keycode=args.power_keycode)
signal.signal(signal.SIGTERM, lambda signum, frame: key_filter_impl.Terminate)
key_filter_impl.Run()
if __name__ == '__main__':
main()