New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace key-chord mode with hydra #129

Closed
legendre6891 opened this Issue May 22, 2015 · 13 comments

Comments

Projects
None yet
3 participants
@legendre6891

legendre6891 commented May 22, 2015

Is it possible to replace key-chord mode with hydra.

For example, I am trying to replicate the following behavior:

(key-chord-define evil-insert-state-map "jk" 'evil-normal-state)

with

(defhydra hydra-change-mode (:color blue)
  "change mode"
  ("k" evil-normal-state "change to normal state"))
(define-key evil-insert-state-map (kbd "j") 'hydra-change-mode/body)

but the problem is that if I take j*, where * is any character not equal to k, then the the head exits without inserting j* in the buffer.

Is there a way to attach test whether a hydra exits via a blue keypress vs. a foreign key?

@joedicastro

This comment has been minimized.

Show comment
Hide comment
@joedicastro

joedicastro May 22, 2015

You can achieve that modifying the behaviour using :post, something like this:

(defhydra hydra-change-mode (:color blue
                             :post (when (eq evil-state 'insert)
                                     "If you exit the hydra without change mode, insert `j`"
                                     (insert "j"))
                             :idle 1)
  "change mode"
  ("j" (insert "j") "insert j")
  ("k" evil-normal-state "change to normal state"))
(define-key evil-insert-state-map (kbd "j") 'hydra-change-mode/body)

You have to use the j head to avoid to call another hydra when you want to type jj (you never know). And the idle 1 is to avoid to show the hydra when you are typing (makes it slower, because it stops you every time it shows) but at the same time you could see it if you wait 1 second.

I could suggest you this head too:

  ("?" (insert "k") "insert k")

The ? is a suggestion of another improbable combination, j? (I think than more than jk) to insert jk as needed.

joedicastro commented May 22, 2015

You can achieve that modifying the behaviour using :post, something like this:

(defhydra hydra-change-mode (:color blue
                             :post (when (eq evil-state 'insert)
                                     "If you exit the hydra without change mode, insert `j`"
                                     (insert "j"))
                             :idle 1)
  "change mode"
  ("j" (insert "j") "insert j")
  ("k" evil-normal-state "change to normal state"))
(define-key evil-insert-state-map (kbd "j") 'hydra-change-mode/body)

You have to use the j head to avoid to call another hydra when you want to type jj (you never know). And the idle 1 is to avoid to show the hydra when you are typing (makes it slower, because it stops you every time it shows) but at the same time you could see it if you wait 1 second.

I could suggest you this head too:

  ("?" (insert "k") "insert k")

The ? is a suggestion of another improbable combination, j? (I think than more than jk) to insert jk as needed.

@legendre6891

This comment has been minimized.

Show comment
Hide comment
@legendre6891

legendre6891 May 22, 2015

Thanks @joedicastro! Your solution works great, but it relies on knowing what k changed the mode. If I had another head that did something else (and stayined in insert mode, say), then the solution would not work.

I suppose a more general solution is to:

  1. Using :pre, set some variable equal to nil.
  2. In each head, set variable to non-nil.
  3. In post, check whether variable is nil to see whether a head was pressed.

Is this possible, or is there a better general solution? (It would be nice if hydra-mode had some sort of builtin
mechanism for this.)

I actually don't know enough elisp to actually implement step 1 and 2, so any help there is appreciated to 😊.

P.S. It is possibel to insert jj and jk without setting up another head by doing C-q j j and C-q j k, respectively (assuming you didn't map C-q to something else). With the ("j" (insert "j") "insert j") head posted above, it is actually impossible to type an odd number of consecutive js -- but I suppose that case never comes up anyhow.

legendre6891 commented May 22, 2015

Thanks @joedicastro! Your solution works great, but it relies on knowing what k changed the mode. If I had another head that did something else (and stayined in insert mode, say), then the solution would not work.

I suppose a more general solution is to:

  1. Using :pre, set some variable equal to nil.
  2. In each head, set variable to non-nil.
  3. In post, check whether variable is nil to see whether a head was pressed.

Is this possible, or is there a better general solution? (It would be nice if hydra-mode had some sort of builtin
mechanism for this.)

I actually don't know enough elisp to actually implement step 1 and 2, so any help there is appreciated to 😊.

P.S. It is possibel to insert jj and jk without setting up another head by doing C-q j j and C-q j k, respectively (assuming you didn't map C-q to something else). With the ("j" (insert "j") "insert j") head posted above, it is actually impossible to type an odd number of consecutive js -- but I suppose that case never comes up anyhow.

@joedicastro

This comment has been minimized.

Show comment
Hide comment
@joedicastro

joedicastro May 22, 2015

@legendre6891 Oh, I didn't know that you expected a more generic case 😄

Well, you proposed solution can be implemented as following:

(defvar my-temporal-var nil)
(defhydra hydra-change-mode (:color blue
                             :pre (setq my-temporal-var nil)
                             :post (when (eq my-temporal-var nil)
                                     "If you exit the hydra without change mode, insert `j`"
                                     (insert "j"))
                             :idle 1)
  "change mode"
  ("k" (progn (evil-normal-state) (setq my-temporal-var t)) "change to normal state"))
(define-key evil-insert-state-map (kbd "j") 'hydra-change-mode/body)

I don't know to much elisp either, so sure that code could be improved.

P.S. It is possibel to insert jj and jk without setting up another head by doing C-q j j and C-q j k, respectively (assuming you didn't map C-q to something else).

Oh, thanks! I didn't know that. 👍

joedicastro commented May 22, 2015

@legendre6891 Oh, I didn't know that you expected a more generic case 😄

Well, you proposed solution can be implemented as following:

(defvar my-temporal-var nil)
(defhydra hydra-change-mode (:color blue
                             :pre (setq my-temporal-var nil)
                             :post (when (eq my-temporal-var nil)
                                     "If you exit the hydra without change mode, insert `j`"
                                     (insert "j"))
                             :idle 1)
  "change mode"
  ("k" (progn (evil-normal-state) (setq my-temporal-var t)) "change to normal state"))
(define-key evil-insert-state-map (kbd "j") 'hydra-change-mode/body)

I don't know to much elisp either, so sure that code could be improved.

P.S. It is possibel to insert jj and jk without setting up another head by doing C-q j j and C-q j k, respectively (assuming you didn't map C-q to something else).

Oh, thanks! I didn't know that. 👍

@legendre6891

This comment has been minimized.

Show comment
Hide comment
@legendre6891

legendre6891 May 22, 2015

Alright thanks @joedicastro! The solution is good for now, but it could be improved in the following way:

  • We have to set up a my-temporal-var for each hydra for which we want to detect exiting with a foreign key.
  • This breaks down if we want to nest hydras.

At a source code level, I think it would be enough if each hydra-*/body simply returned a value that indicated how it exited (i.e. give the value of the foriegn-key that caused the hydra to exit). I digged into the source code, but it wasn't clear how to do this; perhaps @abo-abo would be kind enough to give some pointers? 😄

legendre6891 commented May 22, 2015

Alright thanks @joedicastro! The solution is good for now, but it could be improved in the following way:

  • We have to set up a my-temporal-var for each hydra for which we want to detect exiting with a foreign key.
  • This breaks down if we want to nest hydras.

At a source code level, I think it would be enough if each hydra-*/body simply returned a value that indicated how it exited (i.e. give the value of the foriegn-key that caused the hydra to exit). I digged into the source code, but it wasn't clear how to do this; perhaps @abo-abo would be kind enough to give some pointers? 😄

@joedicastro

This comment has been minimized.

Show comment
Hide comment
@joedicastro

joedicastro May 23, 2015

@legendre6891 Nesting hydras? more hydras? Oh, I see, so the initial problem was only an example... It could be done, but I'm curious, what do you pretend to do with this? cross-calling another hydras, perhaps? 😄

I have a pair of a ideas of how to do this, but maybe @abo-abo could help you with this better.

joedicastro commented May 23, 2015

@legendre6891 Nesting hydras? more hydras? Oh, I see, so the initial problem was only an example... It could be done, but I'm curious, what do you pretend to do with this? cross-calling another hydras, perhaps? 😄

I have a pair of a ideas of how to do this, but maybe @abo-abo could help you with this better.

@legendre6891

This comment has been minimized.

Show comment
Hide comment
@legendre6891

legendre6891 May 23, 2015

@joedicastro Ah I just want to replace key-chord mode with hydra (the latter is more robust, it seems); the replacement needs the feature(s) above 😄

I'm open to hearing your ideas, as well as @abo-abo's.

legendre6891 commented May 23, 2015

@joedicastro Ah I just want to replace key-chord mode with hydra (the latter is more robust, it seems); the replacement needs the feature(s) above 😄

I'm open to hearing your ideas, as well as @abo-abo's.

@joedicastro

This comment has been minimized.

Show comment
Hide comment
@joedicastro

joedicastro May 23, 2015

@joedicastro Ah I just want to replace key-chord mode with hydra (the latter is more robust, it seems); the replacement needs the feature(s) above 😄

You don't need to nest hydras to replace key-chord with hydra 😄 BTW, replace key-chord with hydra makes no sense.

Well, this solution maybe could fit you:

(defhydra hydra-change-mode (:color blue
                             :post (when
                                       (not
                                        (lookup-key hydra-curr-map (this-single-command-keys)))
                                        "Check if the pressed key is in the hydra keymap, if not, it's a foreign key"
                              (insert "j"))
                             :idle 1)
    "change mode"
    ("k" evil-normal-state "change to normal state"))
(define-key evil-insert-state-map (kbd "j") 'hydra-change-mode/body)

It's a solution that works without change anything in hydra 😄 (the hacker's way)

joedicastro commented May 23, 2015

@joedicastro Ah I just want to replace key-chord mode with hydra (the latter is more robust, it seems); the replacement needs the feature(s) above 😄

You don't need to nest hydras to replace key-chord with hydra 😄 BTW, replace key-chord with hydra makes no sense.

Well, this solution maybe could fit you:

(defhydra hydra-change-mode (:color blue
                             :post (when
                                       (not
                                        (lookup-key hydra-curr-map (this-single-command-keys)))
                                        "Check if the pressed key is in the hydra keymap, if not, it's a foreign key"
                              (insert "j"))
                             :idle 1)
    "change mode"
    ("k" evil-normal-state "change to normal state"))
(define-key evil-insert-state-map (kbd "j") 'hydra-change-mode/body)

It's a solution that works without change anything in hydra 😄 (the hacker's way)

@legendre6891

This comment has been minimized.

Show comment
Hide comment
@legendre6891

legendre6891 May 23, 2015

Ah that is a good solution 👍 -- did not know about this-single-command-key.

My justification for replacing key-chord mode with hydra is that

  1. hydra mode is more powerful.
  2. More importantly, with key-chord mode I couldn't define key chords that activated only when
    both of the conditions are met
    • evil is in insert-mode
    • the current buffer is python-mode (i.e. is using the python-mode-keymap)

Or more generally, I can't mix n' match specifying which mode (within evil) that key chord should trigger while additionally specifying which key map is active. But this is easily accomplished with hydra via evil-define-key. (Perhaps there is a way to accomplish this in key-chord, but I am not aware of one.)

Nesting hydras may be necessary for key chords involving more than 2 keys, I think.

Anyway, thanks so much for your help. Would still be interested if @abo-abo chimes in, so leaving this open at the moment.

legendre6891 commented May 23, 2015

Ah that is a good solution 👍 -- did not know about this-single-command-key.

My justification for replacing key-chord mode with hydra is that

  1. hydra mode is more powerful.
  2. More importantly, with key-chord mode I couldn't define key chords that activated only when
    both of the conditions are met
    • evil is in insert-mode
    • the current buffer is python-mode (i.e. is using the python-mode-keymap)

Or more generally, I can't mix n' match specifying which mode (within evil) that key chord should trigger while additionally specifying which key map is active. But this is easily accomplished with hydra via evil-define-key. (Perhaps there is a way to accomplish this in key-chord, but I am not aware of one.)

Nesting hydras may be necessary for key chords involving more than 2 keys, I think.

Anyway, thanks so much for your help. Would still be interested if @abo-abo chimes in, so leaving this open at the moment.

@joedicastro

This comment has been minimized.

Show comment
Hide comment
@joedicastro

joedicastro May 23, 2015

@legendre6891 My pleasure 👍

I still think that you are over-thinking this, introducing too much overhead.

You could do something more simple like this, activating key-chord by mode:

(add-hook 'python-mode-hook (key-chord-define evil-insert-state-map "jk" 'evil-normal-state))

joedicastro commented May 23, 2015

@legendre6891 My pleasure 👍

I still think that you are over-thinking this, introducing too much overhead.

You could do something more simple like this, activating key-chord by mode:

(add-hook 'python-mode-hook (key-chord-define evil-insert-state-map "jk" 'evil-normal-state))
@abo-abo

This comment has been minimized.

Show comment
Hide comment
@abo-abo

abo-abo May 23, 2015

Owner

Thanks @joedicastro, you're better at solving this problem, since I don't have experience with key chords or evil.

It's important to completely understand what the problem is, in order to solve it.
Otherwise, we'd be just re-implementing (a possibly worse) key-chord-mode in hydra.

My current opinion is that hydra shouldn't replace key-chord for general Emacs. The reason is that it would add more buggy edge cases, and when hydra has a bug, it can become unpleasant: sometimes up to the point all bindings stop working and Emacs has to be killed from outside. I don't recall a bug report like this, but it happens sometimes when I'm experimenting with the code.

But for evil-mode it should be possible, since you can just use this:

(define-key evil-insert-state-map (kbd "j") 'hydra-change-mode/body)

No bug-prone timers or post-command-hooks: just a plain define-key.

Here's a simple hydra:

(defhydra hydra-change-mode (:color blue
                             :body-pre (insert "j")
                             :idle 1.0)
  ("k" (progn
         (delete-char -1)
         (message "ohai"))))

It will actually insert "j" just as you type it, and then delete it only if you press k afterwards.

Here's one modified slightly to account for prefix arguments, e.g. C-u jk should work, although it's an unlikely situation:

(defhydra hydra-change-mode (:color blue
                             :body-pre
                             (progn
                               (let ((pt (point)))
                                 (let ((buffer-undo-list t))
                                   (call-interactively 'self-insert-command))
                                 (push (cons pt (point))
                                       buffer-undo-list)))
                             :idle 1.0)
  ("k" (progn
         (primitive-undo
          (if (null (car buffer-undo-list)) 2 1)
          buffer-undo-list)
         (message "ohai"))))
Owner

abo-abo commented May 23, 2015

Thanks @joedicastro, you're better at solving this problem, since I don't have experience with key chords or evil.

It's important to completely understand what the problem is, in order to solve it.
Otherwise, we'd be just re-implementing (a possibly worse) key-chord-mode in hydra.

My current opinion is that hydra shouldn't replace key-chord for general Emacs. The reason is that it would add more buggy edge cases, and when hydra has a bug, it can become unpleasant: sometimes up to the point all bindings stop working and Emacs has to be killed from outside. I don't recall a bug report like this, but it happens sometimes when I'm experimenting with the code.

But for evil-mode it should be possible, since you can just use this:

(define-key evil-insert-state-map (kbd "j") 'hydra-change-mode/body)

No bug-prone timers or post-command-hooks: just a plain define-key.

Here's a simple hydra:

(defhydra hydra-change-mode (:color blue
                             :body-pre (insert "j")
                             :idle 1.0)
  ("k" (progn
         (delete-char -1)
         (message "ohai"))))

It will actually insert "j" just as you type it, and then delete it only if you press k afterwards.

Here's one modified slightly to account for prefix arguments, e.g. C-u jk should work, although it's an unlikely situation:

(defhydra hydra-change-mode (:color blue
                             :body-pre
                             (progn
                               (let ((pt (point)))
                                 (let ((buffer-undo-list t))
                                   (call-interactively 'self-insert-command))
                                 (push (cons pt (point))
                                       buffer-undo-list)))
                             :idle 1.0)
  ("k" (progn
         (primitive-undo
          (if (null (car buffer-undo-list)) 2 1)
          buffer-undo-list)
         (message "ohai"))))
@joedicastro

This comment has been minimized.

Show comment
Hide comment
@joedicastro

joedicastro May 23, 2015

@abo-abo The simple hydra is a very clever solution, if we change the message by what @legendre6891 wanted, changing the mode to come back to the normal mode, you have an amazing light solution:

(defhydra hydra-change-mode (:color blue
                             :body-pre (insert "j")
                             :idle 1.0)
  ("k" (progn
         (delete-char -1)
         (evil-normal-state))))
(define-key evil-insert-state-map (kbd "j") 'hydra-change-mode/body)

Works perfectly fine. Thanks Oleh! 👍

The other hydra, is less useful because the prefix arguments works in other way in Evil/Vim and AFAIK almost nobody that uses Evil uses the standard Emacs C-u prefix. Also the Evil prefix works perfectly fine with the simple solution. For example if you wanted to write jim 5 times you could do it in this way: 5ijimESC. What @legendre6891 pretends is to use the chord jk to avoid to press ESC to come back to the normal mode from the insert mode (I personally prefer to map the Caps Lock key as an additional Ctrl key and the use the xcape tool to use that key as an ESC key when is pressed alone). The Vimmers and formal Vimmers (like me) are kinda obsessed with try to keep the fingers the most time in the main row and avoid to use the "suburban" keys. 😄 Anyway, how that hydra is implemented could become handy in another solutions as well, Thanks again!

joedicastro commented May 23, 2015

@abo-abo The simple hydra is a very clever solution, if we change the message by what @legendre6891 wanted, changing the mode to come back to the normal mode, you have an amazing light solution:

(defhydra hydra-change-mode (:color blue
                             :body-pre (insert "j")
                             :idle 1.0)
  ("k" (progn
         (delete-char -1)
         (evil-normal-state))))
(define-key evil-insert-state-map (kbd "j") 'hydra-change-mode/body)

Works perfectly fine. Thanks Oleh! 👍

The other hydra, is less useful because the prefix arguments works in other way in Evil/Vim and AFAIK almost nobody that uses Evil uses the standard Emacs C-u prefix. Also the Evil prefix works perfectly fine with the simple solution. For example if you wanted to write jim 5 times you could do it in this way: 5ijimESC. What @legendre6891 pretends is to use the chord jk to avoid to press ESC to come back to the normal mode from the insert mode (I personally prefer to map the Caps Lock key as an additional Ctrl key and the use the xcape tool to use that key as an ESC key when is pressed alone). The Vimmers and formal Vimmers (like me) are kinda obsessed with try to keep the fingers the most time in the main row and avoid to use the "suburban" keys. 😄 Anyway, how that hydra is implemented could become handy in another solutions as well, Thanks again!

@legendre6891

This comment has been minimized.

Show comment
Hide comment
@legendre6891

legendre6891 May 24, 2015

Ah @abo-abo's solutions above are actually quite nice. The use of :body-pre solves the problem.
And thanks again, @joedicastro; however

(add-hook 'python-mode-hook (key-chord-define evil-insert-state-map "jk" 'evil-normal-state))

doesn't quite work, because this puts jk into the evil-insert-state-map once I visit any python file (e.g. if I visit a .pl file afterwards, the binding jk is still present).

Anyway, thanks everyone!

P.S. The feature :body-pre is only documented on @abo-abo's blog posts -- it does not have documention here on github. Are there plans to update the documentation?

legendre6891 commented May 24, 2015

Ah @abo-abo's solutions above are actually quite nice. The use of :body-pre solves the problem.
And thanks again, @joedicastro; however

(add-hook 'python-mode-hook (key-chord-define evil-insert-state-map "jk" 'evil-normal-state))

doesn't quite work, because this puts jk into the evil-insert-state-map once I visit any python file (e.g. if I visit a .pl file afterwards, the binding jk is still present).

Anyway, thanks everyone!

P.S. The feature :body-pre is only documented on @abo-abo's blog posts -- it does not have documention here on github. Are there plans to update the documentation?

@abo-abo

This comment has been minimized.

Show comment
Hide comment
@abo-abo

abo-abo May 24, 2015

Owner

The feature :body-pre is only documented on @abo-abo's blog posts -- it does not have documention here on github. Are there plans to update the documentation?

Just added :body-pre.

Owner

abo-abo commented May 24, 2015

The feature :body-pre is only documented on @abo-abo's blog posts -- it does not have documention here on github. Are there plans to update the documentation?

Just added :body-pre.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment