Post

kmonad -- Using Capslock as both Esc and Ctrl

在使用vim的时候,我已经习惯了将esccapslock互换位置,不得不说是真的爽。

而对于emacs派,则流行将left ctrlcapslock呼唤,这样按C-a, C-b, C-f等快捷键就不会那么痛苦。

去年我试过一个keymap软件(名字忘了)来实现vim流的映射, 但结果一言难尽 —— 在vscode里,互换就不会生效,还有其它的一些小bug, 不久就被我遗弃了。 最近发现了一个haskell写的keymap软件 – kmonad, 试了之后惊为天人,完美解决了曾经的bug, 可以实现全局映射。

临时写了一个笨拙的映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(defcfg
  input  (device-file "/dev/input/by-path/platform-i8042-serio-0-event-kbd")
  output (uinput-sink "kmonad output")
  fallthrough true
  allow-cmd false
)

(defsrc
  esc f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 pause prnt ins del
  ` 1 2 3 4 5 6 7 8 9 0 - = bspc home
  tab q w e r t y u i o p [ ] ret pgup
  caps a s d f g h j k l ; ' \ pgdn
  lsft z x c v b n m , . / rsft up end
  lctl lmet lalt spc ralt cmps rctl left down rght
)

(deflayer base
  caps f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 pause prnt ins del 
  ` 1 2 3 4 5 6 7 8 9 0 - = bspc home
  tab q w e r t y u i o p [ ] ret pgup
  esc a s d f g h j k l ; ' \ pgdn
  lsft z x c v b n m , . / rsft up end
  lctl lmet lalt spc ralt cmps rctl left down rght
)

其中defsrcdeflayer中的对象一一对应,这里我做的就是把esccaps的映射结果互换。


然而当时我对kmonad的强大还一无所知。直到今天,我突然又有了交换CapslockLeft Ctrl的想法。倒不是因为用emacs, 而是在折腾tmux, 它的默认主键如果能绑定到Ctrl-a,然后用Capslock代替Ctrl,想想就是一件美事啊!

我开始幻想:Capslock这么好的位置,能不能让我同时实现两个映射呢?搜索了一番发现,kmonad竟然也能胜任这个工作,等不及了,直接开干!

这次阅读了官方文档的部分内容:

  • fallthrough
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    fallthrough: `true` or `false`, defaults to `false`
    
      KMonad catches input events and tries to match them to various handlers. If
      it cannot match an event to any handler (for example, if it isn't included
      in the corresponding `defsrc` block, or if it is, but the current keymap
      does not map any buttons to it), then the event gets quietly ignored. If
      `fallthrough` is set to `true`, any unhandled events simply get reemitted.
    
      In more practical terms, this allows you to only specify the keys that
      you want to overwrite in your `defsrc' block. For example, the following
      configuration would rebind Caps Lock to Escape only when tapped.
    
        (defcfg
          input … output …
          fallthrough true)
        (defsrc caps)
        (deflayer my-layer (tap-next esc caps))
    

    fallthroughfalse的时候,我们没有在defsrc等列表中定义的按键在失配后会直接被忽略;但是如果设置为true,则会继续pass, 这样就不用把所有的按键都写一遍了。

  • tap-next, tap-hold, tap-hold-next
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#| --------------------------------------------------------------------------
                          Optional: Multi-use buttons

  Perhaps one of the most useful features of KMonad, where a lot of work has
  gone into, but also an area with many buttons that are ever so slightly
  different. The naming and structuring of these buttons might change sometime
  soon, but for now, this is what there is.

  For the next section being able to talk about examples is going to be handy,
  so consider the following scenario and mini-language that will be the same
  between scenarios:

    - We have some button `foo` that will be different between scenarios
    - `foo` is bound to 'Esc' on the input keyboard
    - the letters a s d f are bound to themselves
    - Px signifies the press of button x on the keyboard
    - Rx signifies the release of said button
    - Tx signifies the sequential and near instantaneous press and release of x
    - 100 signifies 100ms pass

  So for example:
    Tesc Ta:
      tap of 'Esc' (triggering `foo`), tap of 'a' triggering `a`
    Pesc 100 Ta Tb Resc:
      press of 'Esc', 100ms pause, tap of 'a', tap of 'b', release of 'Esc'

  The `tap-next` button takes 2 buttons, one for tapping, one for holding, and
  combines them into a single button. When pressed, if the next event is its own
  release, we tap the 'tapping' button. In all other cases we first press the
  'holding' button then we handle the event. Then when the `tap-next` gets
  released, we release the 'holding' button.

  So, using our mini-language, we set foo to:
    (tap-next x lsft)
  Then:
    Tesc            -> x
    Tesc Ta         -> xa
    Pesc Ta Resc    -> A
    Pesc Ta Tr Resc -> AR

  The `tap-hold` button is very similar to `tap-next` (a theme, trust me). The
  difference lies in how the decision is made whether to tap or hold. A
  `tap-hold` waits for a particular timeout, if the `tap-hold` is released
  anywhere before that moment we execute a tap immediately. If the timeout
  occurs and the `tap-hold` is still held, we switch to holding mode.

  The additional feature of a `tap-hold` is that it pauses event-processing
  until it makes its decision and then rolls back processing when the decision
  has been made.

  So, again with the mini-language, we set foo to:
    (tap-hold 200 x lsft) ;; Like tap-next, but with a 200ms timeout
  Then:
    Tesc             -> x
    Tesc Ta          -> xa
    Pesc 300 Ta      -> A (the moment you press a)
    Pesc Ta 300      -> A (after 200 ms)
    Pesc Ta 100 Resc -> xa (both happening immediately on Resc)

  The `tap-hold-next` button is a combination of the previous 2. Essentially,
  think of it as a `tap-next` button, but it also switches to held after a
  period of time. This is useful, because if you have a (tap-next ret ctl) for
  example, and you press it thinking you want to press C-v, but then you change
  your mind, you now cannot release the button without triggering a 'ret', that
  you then have to backspace. With the `tap-hold-next` button, you simply
  outwait the delay, and you're good. I see no benefit of `tap-next` over
  `tap-hold-next` with a decent timeout value.

  You can use the `:timeout-button` keyword to specify a button other than the
  hold button which should be held when the timeout expires. For example, we
  can construct a button which types one x when tapped, multiple x's when held,
  and yet still acts as shift when another button is pressed before the timeout
  expires. So, using the minilanguage and foo as:
    (tap-hold-next 200 x lsft :timeout-button x)
  Then:
    Tesc           -> Tx
    Pesc 100 Ta    -> A (the moment you press a)
    Pesc 5000 Resc -> xxxxxxx (some number of auto-repeated x's)

(tap-next a b) 代表:单独按下映射到a,长按映射到b,显然这个功能就是我想要的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(defcfg                                                                     
  input  (device-file "/dev/input/by-path/platform-i8042-serio-0-event-kbd")
  output (uinput-sink "my kmonad output")                                   
  fallthrough true                                                          
  allow-cmd false                                                           
)                                                                           
                                                                            
(defsrc                                                                     
  esc caps                                                                  
)                                                                           
                                                                           
(deflayer base                                                              
  caps (tap-next esc lctl)                                                  
)    

Perfect! 直接爽用capslock

当前kmonad正在运行的时候,直接尝试再次读取配置文件的时候会报错,这是因为设备被占用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(venv13) woc@myarch:~ $ sudo kmonad ~/.config/kmonad/map_capslock.kbd
[sudo] password for woc: 
kmonad: Could not perform IOCTL grab on: /dev/input/by-path/platform-i8042-serio-0-event-kbd
(venv13) woc@myarch:~ $ ll /dev/input/by-path/platform-i8042-serio-0-event-kbd
lrwxrwxrwx 1 root root 9 Nov 27 19:06 /dev/input/by-path/platform-i8042-serio-0-event-kbd -> ../event2
(venv13) woc@myarch:~ $ sudo fuser -v /dev/input/event2
                     USER        PID ACCESS COMMAND
/dev/input/event2:   root          1 F.... systemd
                     root        518 F.... systemd-logind
                     woc         819 F.... Hyprland
                     root      31888 f.... kmonad
(venv13) woc@myarch:~ $ sudo lsof /dev/input/event2
lsof: WARNING: can't stat() fuse.portal file system /run/user/1000/doc
      Output information may be incomplete.
COMMAND     PID USER  FD   TYPE DEVICE SIZE/OFF NODE NAME
systemd       1 root 173u   CHR  13,66      0t0  183 /dev/input/event2
systemd-l   518 root  24u   CHR  13,66      0t0  183 /dev/input/event2
systemd-l   518 root  40u   CHR  13,66      0t0  183 /dev/input/event2
Hyprland    819  woc  28u   CHR  13,66      0t0  183 /dev/input/event2
kmonad    31888 root  56r   CHR  13,66      0t0  183 /dev/input/event2

重启kmonad服务解决(刚开始一直找kmonad.service没找到,后来想起来是我自定义的service,不叫这个名字…)

This post is licensed under CC BY 4.0 by the author.