Linuxでホットキーを検出する

ホットキーとかショートカットキーとかキーコンビネーションとか、キーボードのいくつかのキーが同時に押さたことをバックグラウンドで検知する方法です。先に自分で作らずともサードパーティ製モジュールがあることをお知らせしておきます。このほかにも同じような機能のモジュールがいくつかあるとおもいますが、困ったことに執筆時点でキーボードの抜き差しにうまく対応できるものが見つかりませんでした。

さて、前回の記事で入力デバイスイベントファイルが作られることを監視してキーボードの抜き差しを検出していましたが、今回はその中身を解析してキーイベントを検出します。

まずは、キーボードのイベントファイルの中身を眺めてみましょう。指定するイベントファイルevent0は環境によって名前が違うかもしれません。コマンドを実行して適当なキーを押す、とつらつら呪文が流れてきます。

$ od -tx1z /dev/input/event0
0000000 1c 71 e7 5d 65 45 0d 00 04 00 04 00 14 00 07 00  >.q.]eE..........<
0000020 1c 71 e7 5d 65 45 0d 00 01 00 10 00 01 00 00 00  >.q.]eE..........<
0000040 1c 71 e7 5d 65 45 0d 00 00 00 00 00 00 00 00 00  >.q.]eE..........<
0000060 1c 71 e7 5d de 7d 0e 00 04 00 04 00 14 00 07 00  >.q.].}..........<
0000100 1c 71 e7 5d de 7d 0e 00 01 00 10 00 00 00 00 00  >.q.].}..........<

パッと見わかりませんが、各イベントはPythonで書くとデータ構造をしています。

class InputEvent(Structure):
    _fields_ = (
        ('time', TimeVal),
        ('type', c_uint16),
        ('code', c_uint16),
        ('value', c_int32),
    )

そして、入れ子のTimeValはCでおなじみの固定小数点の時刻構造体です。

class TimeVal(Structure):
    _fields_ = (
        ("tv_sec", c_long),
        ("tv_usec", c_long),
    )

このInputEvent構造体のtypeが0x01のときにキーボードイベント、valueが0ならキーアップ、1ならキーダウン、2なら長押しです。そしてcodeがキーコード。Linuxのキーコードの定義は /usr/include/linux/input-event-codes.h に書いてあります。

これを解析すると、どのキーが押されていて、どのキーが離されたかがわかります。以下実装例です。長いのはキーコードの定義で、やっていることは後半の40行くらいです。


#
# Event types
#
types = {
    0x01 : 'KEY'
}

#
# Keys and buttons
#
keys = {
    0   : 'RESERVED',
    1   : 'ESC',
    2   : '1',
    3   : '2',
    4   : '3',
    5   : '4',
    6   : '5',
    7   : '6',
    8   : '7',
    9   : '8',
    10  : '9',
    11  : '0',
    12  : 'MINUS',
    13  : 'EQUAL',
    14  : 'BACKSPACE',
    15  : 'TAB',
    16  : 'Q',
    17  : 'W',
    18  : 'E',
    19  : 'R',
    20  : 'T',
    21  : 'Y',
    22  : 'U',
    23  : 'I',
    24  : 'O',
    25  : 'P',
    26  : 'LEFTBRACE',
    27  : 'RIGHTBRACE',
    28  : 'ENTER',
    29  : 'LEFTCTRL',
    30  : 'A',
    31  : 'S',
    32  : 'D',
    33  : 'F',
    34  : 'G',
    35  : 'H',
    36  : 'J',
    37  : 'K',
    38  : 'L',
    39  : 'SEMICOLON',
    40  : 'APOSTROPHE',
    41  : 'GRAVE',
    42  : 'LEFTSHIFT',
    43  : 'BACKSLASH',
    44  : 'Z',
    45  : 'X',
    46  : 'C',
    47  : 'V',
    48  : 'B',
    49  : 'N',
    50  : 'M',
    51  : 'COMMA',
    52  : 'DOT',
    53  : 'SLASH',
    54  : 'RIGHTSHIFT',
    55  : 'KPASTERISK',
    56  : 'LEFTALT',
    57  : 'SPACE',
    58  : 'CAPSLOCK',
    59  : 'F1',
    60  : 'F2',
    61  : 'F3',
    62  : 'F4',
    63  : 'F5',
    64  : 'F6',
    65  : 'F7',
    66  : 'F8',
    67  : 'F9',
    68  : 'F10',
    69  : 'NUMLOCK',
    70  : 'SCROLLLOCK',
    71  : 'KP7',
    72  : 'KP8',
    73  : 'KP9',
    74  : 'KPMINUS',
    75  : 'KP4',
    76  : 'KP5',
    77  : 'KP6',
    78  : 'KPPLUS',
    79  : 'KP1',
    80  : 'KP2',
    81  : 'KP3',
    82  : 'KP0',
    83  : 'KPDOT',
        
    85  : 'ZENKAKUHANKAKU',
    86  : '102ND',
    87  : 'F11',
    88  : 'F12',
    89  : 'RO',
    90  : 'KATAKANA',
    91  : 'HIRAGANA',
    92  : 'HENKAN',
    93  : 'KATAKANAHIRAGANA',
    94  : 'MUHENKAN',
    95  : 'KPJPCOMMA',
    96  : 'KPENTER',
    97  : 'RIGHTCTRL',
    98  : 'KPSLASH',
    99  : 'SYSRQ',
    100 : 'RIGHTALT',
    101 : 'LINEFEED',
    102 : 'HOME',
    103 : 'UP',
    104 : 'PAGEUP',
    105 : 'LEFT',
    106 : 'RIGHT',
    107 : 'END',
    108 : 'DOWN',
    109 : 'PAGEDOWN',
    110 : 'INSERT',
    111 : 'DELETE',
    112 : 'MACRO',
    113 : 'MUTE',
    114 : 'VOLUMEDOWN',
    115 : 'VOLUMEUP',
    116 : 'POWER',  # SC System Power Down
    117 : 'KPEQUAL',
    118 : 'KPPLUSMINUS',
    119 : 'PAUSE',
    120 : 'SCALE',  # AL Compiz Scale (Expose)
        
    121 : 'KPCOMMA',
    122 : 'HANGEUL',
    123 : 'HANJA',
    124 : 'YEN',
    125 : 'LEFTMETA',
    126 : 'RIGHTMETA',
    127 : 'COMPOSE',
        
    128 : 'STOP',  # AC Stop
    129 : 'AGAIN',
    130 : 'PROPS',  # AC Properties
    131 : 'UNDO',  # AC Undo
    132 : 'FRONT',
    133 : 'COPY',  # AC Copy
    134 : 'OPEN',  # AC Open
    135 : 'PASTE',  # AC Paste
    136 : 'FIND',  # AC Search
    137 : 'CUT',  # AC Cut
    138 : 'HELP',  # AL Integrated Help Center
    139 : 'MENU',  # Menu (show menu)
    140 : 'CALC',  # AL Calculator
    141 : 'SETUP',
    142 : 'SLEEP',  # SC System Sleep
    143 : 'WAKEUP',  # System Wake Up
    144 : 'FILE',  # AL Local Machine Browser
    145 : 'SENDFILE',
    146 : 'DELETEFILE',
    147 : 'XFER',
    148 : 'PROG1',
    149 : 'PROG2',
    150 : 'WWW',  # AL Internet Browser
    151 : 'MSDOS',
    152 : 'SCREENLOCK',  # AL Terminal Lock/Screensaver
    153 : 'DIRECTION',
    154 : 'CYCLEWINDOWS',
    155 : 'MAIL',
    156 : 'BOOKMARKS',  # AC Bookmarks
    157 : 'COMPUTER',
    158 : 'BACK',  # AC Back
    159 : 'FORWARD',  # AC Forward
    160 : 'CLOSECD',
    161 : 'EJECTCD',
    162 : 'EJECTCLOSECD',
    163 : 'NEXTSONG',
    164 : 'PLAYPAUSE',
    165 : 'PREVIOUSSONG',
    166 : 'STOPCD',
    167 : 'RECORD',
    168 : 'REWIND',
    169 : 'PHONE',  # Media Select Telephone
    170 : 'ISO',
    171 : 'CONFIG',  # AL Consumer Control Configuration
    172 : 'HOMEPAGE',  # AC Home
    173 : 'REFRESH',  # AC Refresh
    174 : 'EXIT',  # AC Exit
    175 : 'MOVE',
    176 : 'EDIT',
    177 : 'SCROLLUP',
    178 : 'SCROLLDOWN',
    179 : 'KPLEFTPAREN',
    180 : 'KPRIGHTPAREN',
    181 : 'NEW',  # AC New
    182 : 'REDO',  # AC Redo/Repeat
        
    183 : 'F13',
    184 : 'F14',
    185 : 'F15',
    186 : 'F16',
    187 : 'F17',
    188 : 'F18',
    189 : 'F19',
    190 : 'F20',
    191 : 'F21',
    192 : 'F22',
    193 : 'F23',
    194 : 'F24',
        
    200 : 'PLAYCD',
    201 : 'PAUSECD',
    202 : 'PROG3',
    203 : 'PROG4',
    204 : 'DASHBOARD',  # AL Dashboard
    205 : 'SUSPEND',
    206 : 'CLOSE',  # AC Close
    207 : 'PLAY',
    208 : 'FASTFORWARD',
    209 : 'BASSBOOST',
    210 : 'PRINT',  # AC Print
    211 : 'HP',
    212 : 'CAMERA',
    213 : 'SOUND',
    214 : 'QUESTION',
    215 : 'EMAIL',
    216 : 'CHAT',
    217 : 'SEARCH',
    218 : 'CONNECT',
    219 : 'FINANCE',  # AL Checkbook/Finance
    220 : 'SPORT',
    221 : 'SHOP',
    222 : 'ALTERASE',
    223 : 'CANCEL',  # AC Cancel
    224 : 'BRIGHTNESSDOWN',
    225 : 'BRIGHTNESSUP',
    226 : 'MEDIA',
        
    227 : 'SWITCHVIDEOMODE',  # Cycle between available video
                              # outputs (Monitor/LCD/TV-out/etc)
    228 : 'KBDILLUMTOGGLE',
    229 : 'KBDILLUMDOWN',
    230 : 'KBDILLUMUP',
        
    231 : 'SEND',  # AC Send
    232 : 'REPLY',  # AC Reply
    233 : 'FORWARDMAIL',  # AC Forward Msg
    234 : 'SAVE',  # AC Save
    235 : 'DOCUMENTS',
        
    236 : 'BATTERY',
        
    237 : 'BLUETOOTH',
    238 : 'WLAN',
    239 : 'UWB',
        
    240 : 'UNKNOWN',
        
    241 : 'VIDEO_NEXT',  # drive next video source
    242 : 'VIDEO_PREV',  # drive previous video source
    243 : 'BRIGHTNESS_CYCLE',  # brightness up, after max is min
    244 : 'BRIGHTNESS_ZERO',  # brightness off, use ambient
    245 : 'DISPLAY_OFF',  # display device to off state
        
    246 : 'WIMAX',
}

# Range 248 - 255 is reserved for special needs of AT keyboard driver

actions = {
    0 : 'UP',
    1 : 'DOWN',
    2 : 'HELD',
}

#############

import sys
import io
from ctypes import Structure, sizeof, c_long, c_int32, c_uint16


class TimeVal(Structure):
    _fields_ = (
        ("tv_sec", c_long),
        ("tv_usec", c_long),
    )


class InputEvent(Structure):
    _fields_ = (
        ('time', TimeVal),
        ('type', c_uint16),
        ('code', c_uint16),
        ('value', c_int32),
    )


event_size = sizeof(InputEvent)

# イベントを読み込んで構造体を作って返す関数
def get_input_event_struct(f):
    ie = io.BytesIO(f.read(event_size))
    struct_ie = InputEvent()
    ie.readinto(struct_ie)
    return struct_ie

# 押されているキーを格納する集合
pressed_keys = set()

# ホットキーを検知する関数
def detect_hotkey(hotkeys_dict, key, action):
    global pressed_keys
    # キーが放されたら集合から消す
    if action == 'UP':
        pressed_keys.remove(key)
    # キーが押されたら集合に加える
    elif action == 'DOWN':
        pressed_keys.add(key)
    else: # 長押しは無視
        return None

    # 押されているキーがホットキーとマッチするか確認
    for message, hotkey in hotkeys_dict.items():
        if hotkey == pressed_keys:
            # マッチしていたら対応するメッセージを返す
            return message 
    return None


if __name__ == '__main__':
    # 入力デバイスイベントファイル
    dev_input_file = '/dev/input/event0'
    # メッセージに対応するホットキー集合のマッピング
    hotkeys_dict = {'A kind of magic.' : {'F12'},
                    'Come on!' :         {'ESC'},
                    'Powerrrr!!' :       {'LEFTCTRL', 'E'},
                    'God mode...' :      {'SPACE', 'G', 'O', 'D'}}
    print('Hotkeys:', list(hotkeys_dict.values()))

    # イベントファイルをオープン
    with open(dev_input_file, mode='rb', buffering=1) as f:
        # 無限ループで監視し続けること
        while True:
            # バイト列を構造体に入れる
            struct_ie = get_input_event_struct(f)
            # キーイベントかどうかの判定
            if struct_ie.type == 0x01: # key event type
                # キーコードからキー名に変換
                key = keys.get(struct_ie.code)
                if key is None:
                    continue 
                # バリューからアクション名に変換
                action = actions.get(struct_ie.value)
                if action is None:
                    continue 

                # ホットキー検出関数に渡す
                message = detect_hotkey(hotkeys_dict, key, action)
                if message is not None:
                    print(message)

これをPython3で実行し、書いてあるホットキーを押して見ると…

$ python3 key_parser.py 
Hotkeys: [{'F12'}, {'ESC'}, {'E', 'LEFTCTRL'}, {'O', 'G', 'SPACE', 'D'}]
A kind of magic.
Come on!
Powerrrr!!
God mode...

ね、かんたんでしょ。

ちなみにキーボードが抜かれると例外が飛んでくるので、それをハンドリングすれば前回の記事の内容と組み合わせて、キーボードが差されるまで待つことができます。

Traceback (most recent call last):
  File "./key_parser.py", line 338, in <module>
    struct_ie = event_parser(f)
  File "./key_parser.py", line 304, in event_parser
    ie = io.BytesIO(f.read(event_size))
OSError: [Errno 19] No such device

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*