Skip to content
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

feature request: custom command keymaps for certain modes #133

Open
spiderbit opened this issue Nov 7, 2021 · 20 comments
Open

feature request: custom command keymaps for certain modes #133

spiderbit opened this issue Nov 7, 2021 · 20 comments

Comments

@spiderbit
Copy link

spiderbit commented Nov 7, 2021

I have used a lot of workarounds to customize xah-fly-keys for certain modes especially in context with exwm, but not only with exwm, just with normal modes.

  1. is to use with-eval-after-load to setup some custom keybinding with remapping like that:
(with-eval-after-load 'gnus
  (define-key gnus-summary-mode-map (kbd "u") #'xah-fly-insert-mode-activate))

or

(with-eval-after-load 'deft
  (define-key deft-mode-map [remap xah-beginning-of-line-or-block] #'deft-delete-file)
  (define-key deft-mode-map [remap forward-word] #'deft-rename-file))
  1. adding custom keybindings that depending on mode do different stuff for the same key like that:
    (:map xah-fly-e-keymap
	  ("e" . (lambda ()
		   (interactive)
		   (cond
			 (org-src-mode (org-edit-src-exit))
			 (exwm-edit-mode (exwm-edit--finish))
			 ((string= "Firefox" exwm-class-name) (exwm-edit--compose))
			 ((symbol-value with-editor-mode) (with-editor-finish))
			 ((equal mode-name "Org") (org-edit-special))
			 (t (execute-kbd-macro (kbd "C-c C-c"))))))
  1. or using some hooks or advices to delete some key bindings on a mode where you don't need it like that:

(setq xah-fly-command-map (assq-delete-all ?q xah-fly-command-map))

  1. you can often use the insert mode to access the mode specific key bindings, but that is not always practical.

So I wonder if it would not be better to have the ability to define some alist where you add your modes with keybindings like that:

(defvar my-org-command-mode-map (make-keymap) "my custom org-mode keymap.")
(define-key my-org-command-mode-map (kbd "n") 'org-next-visable-heading)
(define-key my-org-command-mode-map (kbd "h") 'org-previous-visable-heading)
(define-key my-org-command-mode-map (kbd "d") 'org-next-block)
(define-key my-org-command-mode-map (kbd "s") 'org-previous-block)
(setq exwm-custom-command-modes '((org-mode . my-org-command-mode-map)))

Partially I did implement that statically in #132 but it's a bit a hack and should maybe not be exwm-specific.

@dvdkhlng
Copy link

Yes, I just asked myself similar questions when posting issue #137 .

I wonder whether this needs to be looked at from the other side: you want to modify org-mode, so you should add your bindings to the org-mode's keymap only. Then the binding you added to org-mode's keymap needs to be specific to command-mode being enabled.

I can think of two ways to do that:

  • use a binding that queries xah-fly-insert-state-p and depending on value invokes your action, or "falls through" to the original binding
    • it's tricky though to implement this "fall through" in an easy way
  • treat "command mode" as a sticky modifier key. This requires rewriting most of xah-fly-keys, though it seems to simplify all those keymap considerations considerably:
    • when command-mode is active, key-translation-map is used to attach a modifier key to all keys used by xah-fly-key's command mode, before being interpreted by Emacs' keymaps. Let's for now assume modifer "Hyper" is attached (see the example here)
    • then command-mode would not add any minor-mode keymaps. Instead we have a number of global and mode-specific bindings for keys that have the Hyper modifier. E.g. instead of putting ("SPC" . xah-fly-leader-key-map) into xah-fly-command-map, we just put ("H-SPC" . xah-fly-leader-key-map") into the global key map.
    • if you want to override the "n" key when in org-mode when command-mode is active, just add a binding for "H-n" to org-mode's keymap.

@spiderbit
Copy link
Author

spiderbit commented Feb 18, 2022

I would be open to have / write / manage such a fork because what I use now is a ugly messy buggy hack I don't really like. And it's very important to me to keep emacs hackable and not loosing that just because I use exwm.

I mean sure exwm will be always no ideal solution I can't define application specific keybindings or use command mode in a text box. But as long as we can't do everything in native emacs exwm is the best we can get.

So basically that lines:

   ("a" . xah-fly-M-x)
   ("b" . isearch-forward)
   ("c" . previous-line)

Would be then:

   ("H-a" . xah-fly-M-x)
   ("H-b" . isearch-forward)
   ("H-c" . previous-line)

and then what something like:

(unless (boundp xah-fly-exwm-command-map <key-pressed>)
      (... call-interactive <H key-pressed>))

@dvdkhlng
Copy link

dvdkhlng commented Feb 18, 2022

I think you're somewhat ahead to me in terms of having moved into a emacs-only working environment. About the mess that xah-fly made of all the other modes, I'm starting to like the "sticky hyper key" approach to mode switching and may try to do a prototype next week.

Though in general, with the problems you describe, (partially) reimplementing xah-fly-keys via a custom keyboard firmware (see e.g. QMK) may be the less insane aproach :)

@spiderbit
Copy link
Author

@dvdkhlng just updated my last comment, just say it in case you got not noticed.

@dvdkhlng
Copy link

dvdkhlng commented Feb 18, 2022

and then what something like:

(unless (boundp xah-fly-exwm-command-map <key-pressed>)
      (... call-interactive <H key-pressed>))

I'm not sure I understand what you're up to, but I think that the "hyper" modification can automatically be dropped directly inside the key-translation-map handler as required (e.g. when detecting current buffer is in exwm mode). Or alternatively drop the Hyper modifier when no current binding matches.

Right now I'm not sure which keymap to attach those hyper bindings to. Putting them in the global map would affect exwm mode, so I wonder whether there is some less global keymap that would make more sense? As all the major modes seem to form a hierarchy (c-mode is a child of prog-mode is a child of text-mode), maybe there is a small set of major mode maps that we could attach to, which would imply that exwm buffers would not be affected.

@spiderbit
Copy link
Author

spiderbit commented Feb 18, 2022

But I want some keys to work in exwm buffers like:

	  ("<kp-4>" . 'eshell)
	  ("<kp-5>" . 'shell)
          ("<XF86Launch6>" . (lambda () (interactive)
			       (find-file user-init-org)))

it's just not all, I use normal number keys if I want to type numbers so I want them basically in command mode at least but also in insert mode. Which currently I have to write either:

(push 'XF86Launch6 exwm-input-prefix-keys)

So that exwm does not shadow this key to xah-fly-keys or I have to bind it to the xah-fly-exwm-command-map.

basically I can't bind it to exwm-keymap because I want this key bindings sometimes depending on insert mode/ command mode somethimes I want them active in both modes.

@spiderbit
Copy link
Author

@dvdkhlng Yes I think having some prototype code would be very nice.

keyboard firmware might be easier I don't know but not very helpful because not everybody has a keyboard compatible with such firmware.

@dvdkhlng
Copy link

dvdkhlng commented Feb 18, 2022

This proof of concept seems to work. edit fixed hyperify and made it properly work on numeric and symbol-typed events.

;; the "command mode" flag, i.e. whether our sticky hyper key is pressed.
(defvar hyper-lock nil)

;; For testing, F12 toggles our sticky "hyper" key.
(global-set-key [f12] (lambda ()
                        (interactive)
                        (setq hyper-lock (not hyper-lock))
                        (message "hyper-lock %s" hyper-lock)))

;; Key translation handler that adds Hyper to the first event in the sequence
;; but only if that yields a valid key binding in the  current context.
(defun hyperify (prompt)
  (let* ((v (this-command-keys-vector))
         (len (length v))
         (ev1 (aref v (1- len)))
         (ev1v (vector ev1)))
    (if (or (not hyper-lock) (/= 1 len))
        ev1v
      (let ((ev1mod (event-modifiers ev1))
             (ev1base (event-basic-type ev1)))
        (if (memq 'hyper ev1mod)
            ev1v
          (vector
           (event-convert-list
            (nconc (cons 'hyper ev1mod) (cons ev1base nil)))))))))

(define-key key-translation-map "r" 'hyperify)
(define-key key-translation-map "n" 'hyperify)

;; Bind H-n, but do not bind H-r so we can verify that hyperify only affects
;; key bindings that are in use.
(global-set-key [(hyper ?n)] (lambda() (interactive)( insert "hyper!")))

So then we'd only put the xah-fly special bindings into text-mode's map and maybe some of them into special-mode's map (maybe also into some minor-mode relevant maps?).

As exwm does not inherit from either of those major modes, it should not receive any of the hyper-bindings and would thus not be affected by the newly added sticky hyper key (unless you manually add bindings into exwm's map or add bindings to the global map or to any minor mode maps that are active when you use exwm).

@dvdkhlng
Copy link

dvdkhlng commented Feb 19, 2022

So I rewrote the key-binding machinery of xah-fly-keys to not change the set of active key-maps, but solely rely on key translation. Warning: this is still very experimental, make backups of your files and don't blame me for lost work/time/nerves.

https://github.com/dvdkhlng/xah-fly-keys-hyper

When activating xah-fly-keys minor mode, this now installs a key translation handler that adds "hyper-" modifier to the keys listed in xah-fly-translated-keys (translated from dvorak to whatever keyboard you configured).

xah-fly-command-map now lists all command-mode keys with the hyper modifier (implicitely added when you define the keys using xah-fly--define-command-keys.). These bindings can now exist all the time, they won't be triggered in insert mode, as no hyper modifier gets attached to key events in insert mode. Note that hyper modifier is only added to events, if a valid binding exists for the hyper-modified event. So in contexts, where no command mode binding is active, the keys automatically fall back to their original function (e.g. in exwm-mode).

The xah-fly-command-map is not installed as a minor mode map, as that would again interfere with major mode customizability. Instead xah-fly-command-map gets injected into the following keymaps:

  • text-mode-map
  • prog-mode-map
  • special-mode-map
  • minibuffer-local-map

For testing, you can e.g. visit a file using fundamental-mode, which does not have any mode map of its own. So none of the command mode bindings will be available.
When that behaviour is unsatisfying, try to do

 (xah-fly-keymap-inject (current-global-map) xah-fly-command-map))

Which will make command mode take effect (almost) everywhere (no, I haven't verified that this works).

So where to go from here?

edit in case there are too many people/modes who/that rely on hyper bindings for their standard operation, we may want to use a different set of modifiers that are unlikely to be used by human operators. Such as "hyper-super-meta-control-".

@spiderbit
Copy link
Author

@dvdkhlng oh you were very busy :D give me a second I test it later very interesting

@dvdkhlng
Copy link

Trying to use this productively, I just found at least 3 bugs, two of them I just pushed fixes for to the above repo (isearch broken and major-mode keymap injection failed depending on load order).

The remaining bug seems to be about how key-translation-map operates in emacs: while I do not add the hyper modifier for subsequent events in a key sequence, it is still added for the sequence DEL DEL in command mode (which for me now translates to the unbound command H-DEL H-DEL). No idea, what's causing this. This also seems to be related to the fact that I have a global-map binding for DEL that's bound to xah-fly-command-mode-activate

@dvdkhlng
Copy link

Ah yes, and the keymap injection is broken, in that any modes adding stuff to any of the keymaps after we modified them, those modes will accidentally inject into our command mode map instead ,causing quite some havock.

@dvdkhlng
Copy link

Ah yes, and the keymap injection is broken, in that any modes adding stuff to any of the keymaps after we modified them, those modes will accidentally inject into our command mode map instead ,causing quite some havock.

latest commit fixes that.

@spiderbit
Copy link
Author

@dvdkhlng so I tried it a bit but struggle to get it in a usable state together with exwm.

As example when I set:

(bind-key (kbd "w") 'xah-next-window-or-frame xah-fly-command-map)

in insert mode w now also changes the window. (I think that was the same in classic exwm)

But still "w" doesn't work for frame change in a exwm-buffer

then I had something like that:

(defun sb/wrapper-active-xfk ()
  (push ?w exwm-input-prefix-keys))

(defun sb/wrapper-deactive-xfk ()
  (setq exwm-input-prefix-keys (remove ?w exwm-input-prefix-keys)))

(progn
  (add-hook 'xah-fly-command-mode-activate-hook 'sb/wrapper-active-xfk)
  (add-hook 'xah-fly-insert-mode-activate-hook 'sb/wrapper-deactive-xfk)
  (add-hook 'change-major-mode-hook 'sb/wrapper-active-xfk))

alternatively there would also be exwm-mode-hook, but the 1 liner at top should work.

@spiderbit
Copy link
Author

spiderbit commented Feb 20, 2022

I trimmed it down I use "?a" which should start "M-x" execute-command or smex or something.

all hook I use don't seem to work with changing to a exwm window except change-major-mode-hook, but if I then press "a" I get the error:
error in process filter: Args out of range: [], -1

Here the code:

(defun sb/wrapper-active-xfk ()
  (push ?a exwm-input-prefix-keys))

(add-hook 'change-major-mode-hook 'sb/wrapper-active-xfk)

That should work with upstream xah-fly-keys

In Messages I see even before a slightly different error:
error in process filter: custom-initialize-reset: Args out of range: [], -1

@spiderbit
Copy link
Author

I mean the goal is as first step to get the "a" and "w" button to work in command mode under exwm buffers like in text buffers, that code I posted is just how I did that in upstream exwm.

For testing you can use exwm also in normal emacs without using it as the desktop environment.

@dvdkhlng
Copy link

dvdkhlng commented Feb 20, 2022

@dvdkhlng so I tried it a bit but struggle to get it in a usable state together with exwm.

(bind-key (kbd "w") 'xah-next-window-or-frame xah-fly-command-map)

Ah no, xah-fly-command-map is now active all the time, and merely the hyper modifier gets added on input when in command mode. So this should look like this:

(bind-key (kbd "H-w") 'xah-next-window-or-frame xah-fly-command-map)

But then, I don't even know the bind-key function (that's not standard elisp?).

You may now also just put these bindings into the global keymap (and override them from the major mode keymaps where required).

in insert mode w now also changes the window. (I think that was the same in classic exwm)

But still "w" doesn't work for frame change in a exwm-buffer

then I had something like that:
[..]

Try adding bindings directly to exwm-mode-map

(require 'exwm)
(define-key exwm-mode-map (kbd "H-w")  'xah-next-window-or-frame)

(it would be cleaner to use eval-after-load but I've no experience using it).

BTW just pushed another round of fixes, after a eating my own dog food today.

@dvdkhlng
Copy link

I mean the goal is as first step to get the "a" and "w" button to work in command mode under exwm buffers like in text buffers, that code I posted is just how I did that in upstream exwm.

For testing you can use exwm also in normal emacs without using it as the desktop environment.

In the xah-fly-keys-hyper implementation, command mode is now just a sticky hyper key affecting first key event in any key sequence. So just forget about xah-fly-command-map and add keyboard bindings for hyper-<KEY> to the usual minor/major/global key maps.

The "sticky hyper modifier key" metaphor is only slightly incorrect in that hyper modifier is omitted, when there is no key binding for the resulting event.

@spiderbit
Copy link
Author

spiderbit commented Feb 20, 2022

@dvdkhlng no that does not work, exwm is more than just another major mode, it fiddles with some fundamentals of emacs, because having X buffers. I am no expert of exwm but you could see it a bit like exwm is the new framework in which emacs buffers and x buffers run.

There is even a special exwm-input-set-key function but that seems not very helpful.
I got this solution to "push" into exwm-input-prefix-keys from the exwm author:

ch11ng/exwm#74

the description of exwm-input-prefix-keys:
"List of prefix keys EXWM should forward to Emacs when in line-mode"

Line mode is always on by default, so basically the keys it should forward to emacs and not send to the X application, otherwise the application manages the key press.

Maybe @ch11ng could help here out, and maybe exwm should be rewritten to work better with this modal modes, but seems to be a much bigger task :D

@dvdkhlng
Copy link

@dvdkhlng no that does not work, exwm is more than just another major mode, it fiddles with some fundamentals of emacs, because having X buffers. I am no expert of exwm but you could see it a bit like exwm is the new framework in which emacs buffers and x buffers run.

Oh god, I had no idea what a mind boggling machinery this exwm is. Just skimmed over the key input handling and the X protocol implementation.

I think that adding support for modal key bindings should be possible, though I need some more time to grok this. They seem to re-implement emacs' keyboard input handling, but they invoke key-translation-map at a too late stage so the hyper- modifiers don't really affect exwm. Specifically for exwm, this needs to move to a lower layer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants