Nagano.rb #9
2022-02-26
とみたまさひろ
Linux デスクトップ環境を使ってる人向け
11月に転職して人生初 Mac
キーマップに慣れない
でも Mac のキーマップの方が良さそう
(Ctrl+N や Ctrl+P がブラウザに取られない)
仕事以外で使ってる Linux でも Mac と同じにしよう!
「最強のキーリマッパー xremap」
https://k0kubun.hatenablog.com/entry/xremap
便利!
Ctrl-N を ↓
Ctrl-P を ↑
Ctrl-F を →
Ctrl-B を ←
Alt-[A-Z] を Ctrl-[A-Z]
みたいな感じにすれば良さそう
Ctrl-K は普通は行末まで削除
でも日本語入力時には Ctrl-K はカタカナ変換にしたい
できなそう…🤔
じゃあ自分で作ってみるか
アプリではなくライブラリ
設定ファイルではなくプログラムを書く必要あり
YAML はつらい…
↓
Ruby の DSL もいいかも…
↓
だったら Ruby プログラム書けばいいんじゃね
ユーザーを input グループに追加すればいいんだけど
セキュリティ的にちょっとこわいかも
sudo なしでやるには
https://github.com/k0kubun/xremap#usage
「Option 2: Run xremap without sudo」
例
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
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
状態ファイルの有無で分岐
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
...
キーロガー的なやつ
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
/dev/input/event*
から24バイト読む
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
};
Rubyで(要root)
ev = File.open('/dev/input/event3')
raw = ev.sysread(24)
sec, usec, type, code, value = raw.unpack('Q!Q!SSl')
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 # 区切り
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' 離す
キーイベントは読めるけどアプリにも渡る
GRAB すればアプリに渡さず横取りできる
ev.ioctl(1074021776, 1) # EVIOCGRAB
キー入力が効かなくなるので注意!
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
キーボードデバイスかどうか
EV_KEY = 0x01 # /usr/include/linux/input-event-codes.h より
buf = ''
ev.ioctl(2147566880, buf) # EVIOCGBIT(0, 1)
buf[0].ord & EV_KEY #=> 1 ならキーボードデバイス
キー A
, Z
に対応しているか
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' に対応
/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 作成
作られたデバイスを evtest で見てみる
% 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)
A
と Z
しか入力できないデバイス
キー入力イベントの作成
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')) # 区切り
毎秒 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
ioctl とか read / write じゃなくて libevdev (evdev gem) もあるんでそれを使うのも良さそう
/dev/input/event*
を GRAB してキーイベントを読む/dev/uinput
で作成したデバイスに書き込む特定のアプリだけで有効にしたいとか無効にしたいとか
X11 で入力フォーカスがあたってるアプリ名の取得
C の場合(ざっくりと):
XGetInputFocus()
でフォーカス Window 取得XGetClassHint()
で Window のクラス名を取得XGetWMName()
でウィンドウタイトルを得るNULL
なら XQueryTree()
で親 Window を得て 2 に戻るRuby には良さそうな X11 ライブラリがなさそう
X11 の全機能を網羅するのは大変そうだし
% ls /usr/share/man/man3 | grep -c '^X.*\.3\.gz$'
1231
C 拡張を書くのもアレなので
Fiddle で libX11 から必要な機能をつまみ食い
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
構造体やポインタも扱える
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 #=> マイクロ秒
必要な関数のみ使えるようにして
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
ざっくりこんな感じ
(ホントは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')]
/dev/input/event*
で入力イベントを読める/dev/input/event*
を GRAB すると入力がアプリに渡らなくなる/dev/uinput
で仮想入力デバイスを作れる