| #!/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() |