2

Is there a way to make simultaneous key presses into a keybinding, e.g. for the keys w, e, f, when pressed within 0.05 seconds of each other, to trigger a command?

To be more specific:

  1. If w, e, f are pressed within 0.05 seconds of each other, then upon the pressing of the last one, XMonad should trigger said command. XMonad should also have intercepted the three keys so that they are not superfluously sent to the focused window.

  2. Otherwise (if at least one of them are not pressed within the 0.05 second time period) XMonad should send the keys to the focused window as usual.

My goal in this is to use w, e, f to "Escape" into a vim-like "Normal Mode", a XMonad.Actions.Submap (submap).


Update with a failed method, in case anyone can see a way to fix it:

I attempted to implement this using submaps, so that, for example, if you pressed w you would end up in chord_mode_w, if you pressed e from there you would end up in chord_mode_we, and if you pressed f from there you would finally end up in normal_mode, for instance. The implementation was very messy: I included, in my main keybindings, something like:

("w", spawn "xdotool key <chord_mode_w_keybinding> ; sleep 0.05 ; xdotool key <abort_keybinding>")
(chord_mode_w_keybinding, chord_mode_w)

for detecting w (the rest would be similar), along with (incomplete) submaps such as:

 chord_mode_w = submap . mkKeymap c $
              [
                      ("e",  chord_mode_we )
                    , ("f",  chord_mode_wf )
                    , (abort_keybinding, pasteString "w")

                    -- in order for the submap to not eat all other letters,
                    -- would need to include all mappings like:
                    , ("a", pasteString "wa")
                    , ("b", pasteString "wb")
                    ...
              ]

 chord_mode_we = submap . mkKeymap c $
               [
                      ("f",  normal_mode )
                    , (abort_keybinding, pasteString "we")


                    -- in order for the submap to not eat all other letters,
                    -- would need to include all mappings like:
                    , ("a", pasteString "wea")
                    , ("b", pasteString "web")
                    ...
               ]

 chord_mode_wf = submap . mkKeymap c $
               [
                      ("e",  normal_mode )
                    , (abort_keybinding, pasteString "wf")

                    -- in order for the submap to not eat all other letters,
                    -- would need to include all mappings like:
                    , ("a", pasteString "wfa")
                    , ("b", pasteString "wfb")
                    ...
               ]

A complete implementation would evidently have been very messy, but in theory should have sent me to normal_mode if I pressed "wef" within 0.05 seconds of each other, aborting and typing out the characters otherwise. There were two problems, however:

  1. pasteString (as well as the other paste functions in XMonad.Util.Paste) is too slow for normal typing

  2. I would end up in normal_mode only a small fraction of the time even if I set the abort delay much higher. Not sure of the reason behind this.

(The reason I used pasteString when aborting instead of spawning another xdotool was that output of xdotool would re-trigger one of the chord_mode_w_keybinding, chord_mode_e_keybinding, chord_mode_f_keybinding, back in the main keybindings, sending me back to the chord modes indefinitely.)

user905686
  • 4,491
  • 8
  • 39
  • 60
spacingissue
  • 497
  • 2
  • 12
  • I don't want to sound arrogant, but i think i don't understand your question. There is an example in the [``Submap`` documentation](http://xmonad.org/xmonad-docs/xmonad-contrib/XMonad-Actions-Submap.html). Do you want to have such a delay (0.05s), or what exactly is it the example doesn't do for you? – deshtop Jan 02 '15 at 20:43
  • I've made an edit that hopefully clarifies the question. Activating a submap was only the example I wanted to use it for; the question is really about how one would go about making "key chord" keybindings. – spacingissue Jan 02 '15 at 21:54

2 Answers2

8

https://hackage.haskell.org/package/xmonad-contrib-0.13/docs/XMonad-Actions-Submap.html

Submap really does do almost what you want (it gets you most of the way there) ... and I will suggest that you may want to change what you are trying to do, ever so slightly, and then Submaps handle it perfectly.

You can configure Submap to capture a w key event, and start waiting for an e which then waits for an f. I even tried this out, and confirmed that it works:

, ((0, xK_w), submap . M.fromList $
    [ ((0, xK_e),    submap . M.fromList $
      [ ((0, xK_f),  spawn "notify-send \"wef combo detected!\"" ) ])
    ])

However, the above is almost certainly not something you'd actually want to do... since it is now impossible to send a w keypress to a window (I had to disable that config before typing this answer, which required sending several w keypress events to the active window)

The behavior I saw just now when playing with this is: if I press w, xmonad traps that event (does not send it to the active window) and is now in a state where it is waiting for either e or something else ... if I press something else, xmonad is no longer in that state, but it does not "replay" those trapped events. So if I press w and then some other key which isn't e, the result is only that xmonad is back out of the state listening for keys in the submap. It does not ever allow a w through to the active window... which I found inconvenient.

Your options as I see it are: 1) settle for the initial keybinding having a modifier, so your multi-key command would be Mod4-w e f 2) find a way to hack the delay logic you described into the action in the submap

I started using a config like this, where I nest conceptually similar actions that are infrequently needed under a tree of nested submaps, analogous to what I pasted above. The root of that tree, however, always has a modifier, so it doesn't steal keypresses which I want to forward to the active window. I use Mod3-semicolon as the root of that tree, and then there are many unmodified keypresses which are just letters (they are mnemonics for the actions).

To me, this seems like a better solution, rather than waiting for a few hundred milliseconds, and then forwarding the events unless they matched. I feel like I would find that annoying, since it would delay any w keypress event...

YMMV, hope it helps someone

Chris Stryczynski
  • 30,145
  • 48
  • 175
  • 286
Blake Miller
  • 805
  • 11
  • 16
  • This is really useful! However I get a problem when defining multiple combinations that start with the same key: only the last combination specified works, all others are ignored - what could be the problem? – user905686 Aug 31 '17 at 20:07
  • What you describe is working for me. May I suggest you ask a new question, and post the code you're using? I think as long as the first key is bound to a submap (as it is in my example) it should work. If you comment here with a link to your question I will try to assist. – Blake Miller Sep 12 '17 at 04:17
  • Thank you, I posted a [new question](https://stackoverflow.com/q/46224259/905686). – user905686 Sep 14 '17 at 16:33
  • Note that there is `submapDefaultWithKey`, which could be used to manually replay the key events when there was no match in the submap. This would still leave the problem, that you could not type "wef", though. – marzipankaiser Mar 20 '20 at 13:17
0

I think the support is newer, but the EZKeyboard stuff makes this crazy easy to setup. It supports multikey keybinds with a much simpler syntax.

See this link as well https://lambdablob.com/posts/xmonad-ez-keyboard-shortcuts/ .

I'm using code like this in my XMonad configuration here: https://github.com/rickprice/dotfiles/tree/main/config/xmonad