摘要:開始動工就我的需求而言,我腦子里第一個浮現的抽象機制就是狀態機,不知為何。一個比較合適的設計是兩層的自動機,第一層用來為這三個一組的事件分組,分好組之后成為第二層狀態機的輸入,一個二元組。
霓虹語標題我都想好了。evdevの力を貸して、Linuxでホットキーの魔改造
Linux用戶就像Minecraft玩家,雖然大家玩的都是Minecraft,但是,臥槽,我們一定是在玩不同的游戲(見到建筑師的MC作品時來自小白的驚嘆)。要讓自己的Linux別人不會用,別人的Linux自己不會用,最重要的當然是要把快捷鍵改得驚天地泣鬼神。
作為一個Vim癮君子,我的需求就是手盡量不要離開主鍵盤區。對,方向鍵我都不想按。于是我想通過一些組合鍵去實現上下左右。有人認為CapsLock按起來方便,我自己比較喜歡按Alt,因為就在空格鍵旁邊,觸手可及。總的來說,我希望Alt+[H|J|K|L]分別變成左下上右,Alt+0變成Home, Alt+4變成End。
我自己試用過很多修改鍵位或者添加熱鍵的工具,包括著名的AutoKey。很可惜,它們大都不好用。比如說,我采取這樣的操作序列Alt Down, K Down, K Up, Alt Up,這些軟件大多會采取在檢測到K Down的時候,同時發出Alt Up, ↑ Down,以便撤銷掉先前一個Alt Down的作用,然后發出↑鍵按下的事件。這里有一個問題,就是很多GUI在Alt Down, Alt Up之后,會喚出菜單,從而失焦于輸入框。
另外一方面就是,真的不是哪里都能用。至少,你不可能拿它去玩賽車游戲。你也不能在TTY中繼續使用這些熱鍵。此外還有許許多多的地方不能使用。熱鍵是一種會上癮的東西,在它失效的時候,你就會有戒斷綜合征。想摔鍵盤。
evdev和uinput我試過從GNOME和X11入手,貌似沒有什么好用的方案。XGrabKeyboard一定程度上可以做到接管的效果,但是要指定窗口,似乎還會讓窗口失焦。總而言之稍微有點太繞。不過Linux最好的一點,就是它很裸露。如果從高于驅動低于X11的層入手,興許會有比較好的效果。
evdev內核中通用的輸入設備驅動,它為設備提供了/dev/input下字符設備接口。它非常底層,內核在進行中斷處理后,第一時間就將輸入數據交由它處理。但是它一點都不反直覺,甚至還提供了很好用的工具libevdev,可以直接用Python處理消息。不少人改游戲手柄都是通過evdev進行的。
uinput是一個特殊的虛擬設備,它允許你直接在用戶態向內核插入輸入事件 —— 一般而言就是直接向/dev/uinput寫數據,不過當然,要服從libevdev提供的接口/數據結構。這些事件之后會在另一個evdev字符設備里,假裝是物理設備的輸入,被X的libinput取出來或者由TTY轉為stdin。
Quick Start這種實驗性的東西就不用C寫了。evdev提供了十分好用的Python Bindings,我們可以直接在Python里寫我們的快捷鍵配置。我們先用pip install evdev安裝它,然后在用它提供的示例腳本來測試一下evdev輸入的究竟是什么東西:sudo python -m evdev.evtest /dev/input/by-path/platform-i8042-serio-0-event-kbd(這里我鍵盤的路徑是i8042鍵盤控制器上的,你需要根據你電腦上的配置來調整)
time 1504189579.19 type 4 (EV_MSC), code 4 (MSC_SCAN), value 21 time 1504189579.19 type 1 (EV_KEY), code 21 (KEY_Y), value 1 time 1504189579.19 --------- SYN_REPORT -------- time 1504189579.28 type 4 (EV_MSC), code 4 (MSC_SCAN), value 21 time 1504189579.28 type 1 (EV_KEY), code 21 (KEY_Y), value 0 time 1504189579.28 --------- SYN_REPORT -------- time 1504189579.29 type 4 (EV_MSC), code 4 (MSC_SCAN), value 18 time 1504189579.29 type 1 (EV_KEY), code 18 (KEY_E), value 1 time 1504189579.29 --------- SYN_REPORT -------- time 1504189579.4 type 4 (EV_MSC), code 4 (MSC_SCAN), value 18 time 1504189579.4 type 1 (EV_KEY), code 18 (KEY_E), value 0 time 1504189579.4 --------- SYN_REPORT -------- time 1504189579.48 type 4 (EV_MSC), code 4 (MSC_SCAN), value 31 time 1504189579.48 type 1 (EV_KEY), code 31 (KEY_S), value 1 time 1504189579.48 --------- SYN_REPORT -------- time 1504189579.64 type 4 (EV_MSC), code 4 (MSC_SCAN), value 31 time 1504189579.64 type 1 (EV_KEY), code 31 (KEY_S), value 0 time 1504189579.64 --------- SYN_REPORT --------
這里我按了yes三個鍵,可以看到,每一個動作(按下或釋放,表現在EV_KEY的value的1或0上),都會產生三個事件,分別是EV_MSC,EV_KEY和EV_SYN。根據 https://lp007819.wordpress.com/2013/02/12/再談linux-input子系統/的說法,事實上是有四個消息的發出,但是第一個通常不被支持(隱身了),第二個MSC_SCAN通常會被應用程序忽略,第三個EV_KEY才是真正會被接收的,第四個是同步,可以看到就是用來產生萌萌的分界線的(笑)
有了這一層認識我們就知道,三個事件合起來,才是一次真正的輸入。
開始動工就我的需求而言,我腦子里第一個浮現的抽象機制就是狀態機,不知為何。一個比較合適的設計是兩層的自動機,第一層用來為這三個一組的事件分組,分好組之后成為第二層狀態機的輸入,一個二元組(key-scan-code, up/down/hold)。第二層狀態機我花了好些時間去構思,結果大概是這樣的:
State | Input Pattern | Transition | Action |
---|---|---|---|
Normal | (Left Alt, Down) | Alt | - |
Normal | ELSE | Normal | inject |
Alt | (J/K/H/L/0/4, *) | Mapped | mapped |
Alt | (Left Alt, Up) | Normal | inject_alt_down, inject_alt_up |
Alt | ELSE | Inject | inject_alt_down, inject |
Inject | (J/K/H/L/0/4, *) | Mapped | inject_alt_up, mapped |
Inject | (Left Alt, Up) | Normal | inject_alt_up |
Inject | ELSE | Inject | inject |
Mapped | (J/K/H/L/0/4, *) | Mapped | mapped |
Mapped | (Left Alt, Up) | Normal | - |
Mapped | ELSE | Inject | inject_alt_down, inject |
是不是頭都暈了。把它畫出來或許會比較清楚,不過我也沒這個閑心拿繪圖軟件再畫一遍了。有四個狀態,分別是:
正常狀態 (Normal),除了Alt以外所有鍵都直接發射到uinput里
剛按了下Alt鍵 (Alt),現在還不能確定Alt鍵會否形成組合鍵
插入狀態 (Inject),不是我們想要的熱鍵,連剛才的Alt一起發射到uinput里
映射狀態 (Mapped),是熱鍵,把映射過之后的鍵發射出去,比如按下了K就發射Up
Inject和Mapped狀態之間轉換時,還要把一些Alt鍵的動作補充一下,以防誤導其它應用程序。
關于evdev本身的使用上,evdev的文檔已經說得非常詳盡。在這個腳本里,僅僅用到了少量的功能,比如從/dev/input里讀事件,我是block read,但是你也可以用select或者epoll去異步完成這些操作。:
# 留意!需要Root權限! dev = evdev.InputDevice("/dev/input/by-path/platform-i8042-serio-0-event-kbd") for event in dev.read_loop(): kev = evdev.categorize(event) ks.input(kev) # 第一層狀態機 process(ks) # 第二層狀態機,以第一層狀態的結果為輸入
Inject這樣的操作就是往uinput里面一次過寫三個事件(含同步):
ui = evdev.UInput() def __inject(keycode, keystate): global ui ui.write(ecodes.EV_MSC, ecodes.MSC_SCAN, keycode) ui.write(ecodes.EV_KEY, keycode, keystate) ui.syn()
當然,最重要的一點是怎樣做到獨占讀,也就是托管整個設備的事件處理不讓它側漏呢?我們為它設置GRAB。Grab了之后,整個系統只有當前進程讀得到鍵盤的輸入。如果這個設備已經被別人Grab了,這個操作就會失敗。
dev.grab() dev.ungrab()
整份代碼已經上傳到 https://github.com/Shihira/la...,歡迎斧正。
參考資料:
https://lp007819.wordpress.com/2013/02/12/再談linux-input子系統/ ,可能需要梯子
https://python-evdev.readthed...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/40793.html
閱讀 1542·2021-11-04 16:10
閱讀 2793·2021-09-30 09:48
閱讀 2843·2019-08-29 11:31
閱讀 1584·2019-08-28 18:22
閱讀 3233·2019-08-26 13:44
閱讀 1324·2019-08-26 13:42
閱讀 2850·2019-08-26 10:20
閱讀 759·2019-08-23 17:00