Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Linux用キーリマッパーを作る技術 / How to make Key Remapper

Linux用キーリマッパーを作る技術 / How to make Key Remapper

とみたまさひろ

February 26, 2022
Tweet

More Decks by とみたまさひろ

Other Decks in Technology

Transcript

  1. 経緯 経緯 11月に転職して人生初 Mac キーマップに慣れない でも Mac のキーマップの方が良さそう (Ctrl+N や

    Ctrl+P がブラウザに取られない) 仕事以外で使ってる Linux でも Mac と同じにしよう! 4
  2. Ctrl-N を ↓ Ctrl-P を ↑ Ctrl-F を → Ctrl-B

    を ← Alt-[A-Z] を Ctrl-[A-Z] みたいな感じにすれば良さそう 6
  3. 例 require 'rkremap' include Rkremap::KeyCode rk = Rkremap.new rk.grab =

    true rk.x11 = true rk.start do |code, mod, app| # Emacs や端末ではそのまま if app.class_name == 'Emacs' || app.class_name =~ /terminal/i rk.key(code, mod) next end # ALT+[A-Z] は Ctrl+[A-Z] に変換 if (mod[KEY_LEFTALT] || mod[KEY_RIGHTALT]) && Rkremap::CODE_KEY[code] =~ /\AKEY_[A-Z]\z/ mod[KEY_LEFTALT] = mod[KEY_RIGHTALT] = false mod[KEY_LEFTCTRL] = true rk.key(code, mod) next end end 11
  4. Ctrl-K 問題 日本語入力中かどうかは fcitx-remote コマンドで判定 while :; do if [

    $(fcitx-remote) -eq 2 ]; then touch /tmp/fcitx-enabled else rm -f /tmp/fcitx-enabled fi sleep 0.1 done 12
  5. 状態ファイルの有無で分岐 if mod[KEY_LEFTCTRL] || mod[KEY_RIGHTCTRL] # Ctrl+K/I/O は日本語変換時はそのまま if code

    == KEY_K && File.exist?('/tmp/fcitx-enabled') rk.key(code, mod) next end # Ctrl+K は行末まで削除 if code == KEY_K rk.key(KEY_END, mod_disable_all.merge({KEY_LEFTSHIFT => true})) rk.key(KEY_X, mod_disable_all.merge({KEY_LEFTCTRL => true})) next end ... 13
  6. キーロガー的なやつ require 'rkremap' def code2key(code) Rkremap::CODE_KEY[code].to_s.sub(/\AKEY_/, '') end rk =

    Rkremap.new rk.grab = false rk.x11 = true rk.start do |code, mod, app| key = (mod.select{|_, v| v}.keys + [code]).map{|c| code2key(c)}.join('-') key << " at #{app.title} [#{app.class_name}]" if rk.x11 puts key end 14
  7. /dev/input/event* から24バイト読む Rubyで(要root) struct input_event { struct timeval time; //

    イベント発生日時 // struct timeval { long int tv_usec, long int tv_nsec }; unsigned short type; // イベントタイプ unsigned short code; // キーコード(キーイベントの場合) unsigned int value; // 0:release / 1:press / 2:repeat }; ev = File.open('/dev/input/event3') raw = ev.sysread(24) sec, usec, type, code, value = raw.unpack('Q!Q!SSl') 17
  8. ThinkPad のキーボードで A を押して離すと: type code value EV_MSC(4) 4 30

    # よくわからん EV_KEY(1) KEY_A(30) 1 # 'A' 押す EV_SYN(0) 0 0 # 区切り EV_MSC(4) 4 30 # よくわからん EV_KEY(1) KEY_A(30) 0 # 'A' 離す EV_SYN(0) 0 0 # 区切り 18
  9. Ctrl や Alt 等の修飾キーも普通のキーと同じ Ctrl+A (EV_KEY だけ抜粋) EV_KEY(1) KEY_LEFTCTRL(29) 1

    # 'CTRL' 押す EV_KEY(1) KEY_A(30) 1 # 'A' 押す EV_KEY(1) KEY_A(30) 0 # 'A' 離す EV_KEY(1) KEY_LEFTCTRL(29) 0 # 'CTRL' 離す 19
  10. evtest % sudo evtest No device specified, trying to scan

    all of /dev/input/event* Available devices: /dev/input/event0: Lid Switch /dev/input/event1: Sleep Button /dev/input/event2: Power Button /dev/input/event3: AT Translated Set 2 keyboard /dev/input/event4: Video Bus /dev/input/event5: Synaptics TM3145-003 /dev/input/event6: ThinkPad Extra Buttons /dev/input/event7: HDA Intel PCH Dock Mic /dev/input/event8: HDA Intel PCH Mic /dev/input/event9: HDA Intel PCH Dock Headphone /dev/input/event10: HDA Intel PCH Headphone /dev/input/event11: HDA Intel PCH HDMI/DP,pcm=3 /dev/input/event12: HDA Intel PCH HDMI/DP,pcm=7 /dev/input/event13: HDA Intel PCH HDMI/DP,pcm=8 /dev/input/event14: HDA Intel PCH HDMI/DP,pcm=9 /dev/input/event15: HDA Intel PCH HDMI/DP,pcm=10 /dev/input/event16: TPPS/2 IBM TrackPoint /dev/input/event17: Integrated Camera: Integrated C 22
  11. キーボードデバイスかどうか キー A, Z に対応しているか EV_KEY = 0x01 # /usr/include/linux/input-event-codes.h

    より buf = '' ev.ioctl(2147566880, buf) # EVIOCGBIT(0, 1) buf[0].ord & EV_KEY #=> 1 ならキーボードデバイス KEY_A = 30 KEY_Z = 44 ev.ioctl(2153792801, buf) # EVIOCGBIT(EV_KEY, (KEY_MAX-1)/8+1) buf.unpack('C*')[KEY_A/8][KEY_A%8] != 0 #=> 'A' に対応 buf.unpack('C*')[KEY_Z/8][KEY_Z%8] != 0 #=> 'Z' に対応 23
  12. /dev/uinput で仮想入力デバイスを作れる(要root) udev = File.open('/dev/uinput', 'w') udev.ioctl(1074025828, EV_KEY) # UI_SET_EVBIT

    キーデバイス udev.ioctl(1074025829, KEY_A) # UI_SET_KEYBIT KEY_A を入力可能 udev.ioctl(1074025829, KEY_Z) # UI_SET_KEYBIT KEY_Z を入力可能 setup = [0x03, 0x1234, 0x5678, 1, 'name', 0].pack('SSSSZ80L') # デバイス情報はてきとー udev.ioctl(1079792899, setup) # UI_DEV_SETUP セットアップ udev.ioctl(21761) # UI_DEV_CREATE 作成 25
  13. 作られたデバイスを evtest で見てみる A と Z しか入力できないデバイス % sudo evtest

    /dev/input/event19 Input driver version is 1.0.1 Input device ID: bus 0x3 vendor 0x1234 product 0x5678 version 0x1 Input device name: "name" Supported events: Event type 0 (EV_SYN) Event type 1 (EV_KEY) Event code 30 (KEY_A) Event code 44 (KEY_Z) 26
  14. キー入力イベントの作成 A を押して離す # 時刻は不要 udev.syswrite(['', EV_KEY, KEY_A, 1].pack('a16SSl')) #

    push A udev.syswrite(['', EV_SYN, 0, 0].pack('a16SSl')) # 区切り udev.syswrite(['', EV_KEY, KEY_A, 0].pack('a16SSl')) # release A udev.syswrite(['', EV_SYN, 0, 0].pack('a16SSl')) # 区切り 27
  15. 毎秒 A-Z をランダムに書き込む迷惑なやつ keys = ('A'..'Z').map{eval "KEY_#{_1}"} while true key

    = keys.sample udev.syswrite(['', EV_KEY, key, 1].pack('a16SSl')) udev.syswrite(['', EV_SYN, 0, 0].pack('a16SSl')) udev.syswrite(['', EV_KEY, key, 0].pack('a16SSl')) udev.syswrite(['', EV_SYN, 0, 0].pack('a16SSl')) sleep 1 end 28
  16. X11 で入力フォーカスがあたってるアプリ名の取得 C の場合(ざっくりと): 1. XGetInputFocus() でフォーカス Window 取得 2.

    XGetClassHint() で Window のクラス名を取得 3. クラス名が得られたら XGetWMName() でウィンドウタイ トルを得る 4. クラス名が NULL なら XQueryTree() で親 Window を 得て 2 に戻る 33
  17. Fiddle Fiddle Ruby から C のライブラリ関数を呼び出せる コンパイル不要 require 'fiddle/import' module

    C extend Fiddle::Importer dlload 'libc.so.6' extern 'int atoi(const char *nptr)' end p C.atoi("123") #=> 123 36
  18. 構造体やポインタも扱える require 'fiddle/import' module C extend Fiddle::Importer dlload 'libc.so.6' typealias

    'time_t', 'long int' typealias 'suseconds_t', 'long int' Timeval = struct(['time_t tv_sec', 'suseconds_t tv_usec']) extern 'int gettimeofday(struct timeval *tv, struct timezone *tz)' end timeval = C::Timeval.malloc(Fiddle::RUBY_FREE) # GC時に解放される C.gettimeofday(timeval, nil) p timeval.tv_sec #=> 1970-01-01 00:00:00 UTC からの経過秒数 p timeval.tv_usec #=> マイクロ秒 37
  19. 必要な関数のみ使えるようにして module X11 extend Fiddle::Importer dlload 'libX11.so.6' typealias 'XID', 'unsigned

    long' typealias 'Window', 'XID' typealias 'Status', 'int' typealias 'Atom', 'unsigned long' Window = struct ['Window window'] Pointer = struct ['void *ptr'] XClassHint = struct ['char *name', 'char *class_name'] XTextProperty = struct ['unsigned char *value', 'Atom encoding', 'int format', 'unsigned long nitems'] extern 'Display* XOpenDisplay(char*)' extern 'int XGetInputFocus(Display*, Window*, int*)' extern 'int XGetClassHint(Display*, Window, XClassHint*)' extern 'Status XQueryTree(Display*, Window, Window*, Window*, Window**, unsigned int*)' extern 'Status XGetWMName(Display*, Window, XTextProperty*)' extern 'int Xutf8TextPropertyToTextList(Display*, XTextProperty*, char***, int*)' extern 'int XFree(void*)' extern 'void XFreeStringList(char**)' end 38
  20. ざっくりこんな感じ (ホントはX11が確保したメモリの解放処理も必要) class_hint = X11::XClassHint.malloc(Fiddle::RUBY_FREE) parent = X11::Window.malloc(Fiddle::RUBY_FREE) children =

    X11::Pointer.malloc(Fiddle::RUBY_FREE) _ = ' '*8 display = X11.XOpenDisplay(nil) w = X11::Window.malloc(Fiddle::RUBY_FREE) X11.XGetInputFocus(display, w, _) win = w.window while win > 0 class_hint.name = class_hint.class_name = nil X11.XGetClassHint(display, win, class_hint) break unless class_hint.name.null? && class_hint.class_name.null? X11.XQueryTree(display, win, _, parent, children, _) win = parent.window end prop = X11::XTextProperty.malloc(Fiddle::RUBY_FREE) text_list = X11::Pointer.malloc(Fiddle::RUBY_FREE) X11.XGetWMName(display, win, prop) X11.Xutf8TextPropertyToTextList(display, prop, text_list, _) p [class_hint.class_name.to_s, text_list.ptr.ptr.to_s.force_encoding('utf-8')] 39
  21. まとめ まとめ /dev/input/event* で入力イベントを読める /dev/input/event* を GRAB すると入力がアプリに渡 らなくなる /dev/uinput

    で仮想入力デバイスを作れる 大きなライブラリの関数をつまみ食いするには Fiddle が便利 40