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

add mode and meow state specific keymaps #126

Closed
wants to merge 1 commit into from

Conversation

turbana
Copy link

@turbana turbana commented Dec 12, 2021

I would like to propose adding meow state specific and mode specific keybinds. So keybinds that would only be active under a specified mode (major or minor) and when under a specific meow state. This would allow binds in any combination of mode and meow state to be active and play nicely with one another.

My thought was to use emulation-mode-map-alists (similar to meow state's minor modes) to accomplish this. For lack of a better name I chose to call these keymaps "overlay maps", let me copy and paste some documentation going over their implementation:

;;; Overlay Maps
;; An "overlay map" is a keymap that should be activated when running under a
;; specific mode (major or minor) and meow state. A keymap is considered an
;; overlay when the vector [meow-overlay] is set as a key in the map. These
;; overlay maps are stored in the mode's keymap under the key
;; [meow-X-state] where X is the meow state. Ex. [meow-insert-state].
;;
;; During setup we add the symbol `meow--user-overlay-maps' to
;; `emulation-mode-map-alists' to facilitate our overlay maps.
;;
;; When meow changes state the following occurs:
;; 1. we look through all active keymaps for any overlay maps.
;; 2. all such overlay maps are combined into a single keymap with
;;    `make-composed-keymap'.
;; 3. we ensure the combined map is accessible under `emulation-mode-map-alists'
;;    by adding a mapping of meow minor mode to keymap to the alist
;;    `meow--user-overlay-maps'.

An example:

(meow-state-define-key 'org-mode-map 'normal
  "S-<left>"    'org-promote-subtree
  "S-<up>"      'org-move-subtree-up
  "S-<down>"    'org-move-subtree-down
  "S-<right>"   'org-demote-subtree
  "C-S-<left>"  'org-do-promote
  "C-S-<right>" 'org-do-demote
  )

Thoughts?

@DogLooksGood
Copy link
Collaborator

Thanks for the contribution, it seems to be both well implemented and well documented.

It's clear to me that a lot users want this feature. Before accepting the PR, I want to have a explanation here.

In an early version of Meow, it is possible to define a mode specific leader keymap in meow, at that moment I believe the leader keymap is for highly used commands. And which commands are highly used depends on current major mode. But I removed the functionality later because I realized that it's not a good idea to have custom keybindings only existing in meow's keymap. I want meow to be something that leverage your vanilla Emacs configuration, not the package that limits how you build your configuration.

That is:

  • meow must not be invasive
  • meow's configuration can be and should be limited in a single .el file, just like other packages
  • remove meow from your configuration should be as simple as deleting a single file

Additionally, these cases are what I'm trying to avoid:

  • configure meow for every major/minor modes
  • removing meow = giving up the effort you spend to organize your keybinding
  • introducing meow = rebuilding your keybind system

So my recommendation is to bind keys in vanilla Emacs. For example:

You want a consistent keybinding SPC t i to toggle images in both org-mode and markdown-mode. You should make a consistent keybinding in vanilla Emacs, and tell meow to use that.

;; assume C-x M-t the prefix for 'toggle'
;; global 'toggle' commands
(global-set-key (kbd "C-x M-t l") 'display-line-numbers-mode)
;; major mode specific 'toggle' commands
(define-key markdown-mode-map (kbd "C-x M-t i") #'markdown-toggle-inline-images)
(define-key org-mode-map (kbd "C-x M-t i") #'org-toggle-inline-images)
;; add entry for 'toggle' in leader
(meow-leader-define-key '("t" . "C-x M-t"))

Then you have consistent keybinding SPC t i in leader and C-x M-t i in vanilla Emacs.

Below configuration are not specific to meow, they are useful whether you use meow or not.


I'd like to have a discussion here. If I can convince others, I'm still inclined to reject this feature.

@turbana
Copy link
Author

turbana commented Dec 13, 2021

Thanks for the compliment and the detailed reply!

You've made a compelling argument for sure. The keybind system is still one of the parts of emacs that I can never quite get fully sunk into by brain. I'm coming from the evil-mode side of emacs (and vim before that) and so was originally trying to replicate the way you can bind keys there (and with general.el being able to specific evil :state). And so I hadn't even really considered having a common prefix key in bare emacs and binding a key in meow to that prefix. One of my goals was to have a local leader key that was mode specific and that neatly solves that problem without further code and in bare emacs. So thanks for that!

The other problem I'm looking to solve that the above doesn't work for is to have a command called under a specific emacs mode AND specific meow state. For example when in org-mode I want the > key to attempt to expand a yasnippet and if not found enter a bare >. I would like that to only occur in insert state and leave all other states open. In evil/general.el I have the following:

;; By convention I title most of my snippets {snippet}> so I would like the
;; > key to try and expand snippets automatically. When no snippet is found
;; a regular > should be entered.
(defun ic/yas-expand-> ()
  (interactive)
  (insert ">")
  (yas-expand))

(general-define-key
 :states 'insert
 :keymaps 'org-mode-map
 ">" 'ic/yas-expand->)

That would be one situation where having a mode and state specific keymap would be handy.

That being said I don't have very many insert state specific commands. Most are for normal state which can be handled in the manner you pointed out. So for those few commands I do want state specific functionality I suppose I can just inspect the meow minor mode. So the above could be rewritten as:

(defun ic/yas-expand-> ()
  (interactive)
  (cond ((bound-and-true-p meow-insert-mode)
         (insert ">")
         (yas-expand))
        ((bound-and-true-p meow-normal-mode)
         ;; normal mode code
         )
        (t
         ;; other mode code, etc
         )))

But I'm happy with only doing that for the few commands I feel need to be state aware and using your method for the bulk of my other keybinds. I can see where having the option to have meow state specific keybinds will lead people to tie their keybinds to meow and make it harder to remove it. I'm certainly finding that to be the case for evil-mode!

So thanks again for the nudge in the right direction for organizing my keybinds "the emacs way". I'm happy to close the PR and look forward to whats next for meow!

@turbana turbana closed this Dec 13, 2021
@DogLooksGood
Copy link
Collaborator

I also have some insert mode specific keybindings. I think my case is very similar to your case mentioned here.

In clojure-mode, I want the semicolon key to insert a colon when the cursor is not at the 1) beginning of line 2) string 3) comment. Otherwise it should still insert the semicolon.

In rust-mode, I want to the minus key to insert a underscore when the cursor is after a word.

here is how I implement this in my configuration

I just bind the command in clojure-mode-map and rust-mode-map. Meow normal state won't be affected in this case.

In fact, if you look at my configuration, you can find meow in just four places.

  • init-modal.el the meow's configuration
  • init-modeline.el my custom mode line format
  • init-rime.el use meow's predication to disable input method in non-insert state
  • A list of themes, for some faces used by meow

@turbana
Copy link
Author

turbana commented Dec 13, 2021

I just bind the command in clojure-mode-map and rust-mode-map. Meow normal state won't be affected in this case.

Thanks for this! It gave me the "ah ha" moment realizing that meow's normal state keymap is part of emulation-mode-map-alist and therefore takes precedence over both local modes and major modes (where insert specific commands can go).

This is where I have trouble fully groking the way emacs looks up keybinds, but it's slowly starting to sink in.

@ChauhanT
Copy link

Hi, not sure if this is the right place to ask. I am trying to remap < in org-mode to 'org-shiftleft. I am trying to do this with general with something like :

(general-define-key
 :keymaps 'org-mode-map
 "<" 'org-shiftleft)

Sadly, this does not work as it also maps the < in the insert mode :( Has anyone been able to tackle this ?

@DogLooksGood
Copy link
Collaborator

This is not supported. The normal state is a general state, there's no mode-specific normal state keymap.

Here the reason and solution is explained: #126 (comment)

@rickalex21
Copy link

@DogLooksGood Hello, I would like to switch to meow but I would like to switch back and forth
between meow and evil until I move everything over to meow.

I was reading your suggestion to use vanilla emacs to define keys but I'm not sure
how to account for normal, insert, and motion. I'm farily new to meow I don't want to
have two configs.

I'm not sure if a macro is the best way to go about this but it should:

  • Switch between evil and meow by setting my/modal.
  • Define keys in normal, insert,visual, and motion.
  • Define keys in major modes.
  • Use leader key, I use space.

Ideally I would like to do an &rest restparameter so I won't have to write
my/define-key on every line, evil can handle the &rest but I would have to
figure that out for meow.

This is would be great:

(setq my/modal 'meow)

(my/define-key 'normal
               (kbd "<leader>z z") (lambda ()(interactive) (message "hi"))
               (kbd "<leader>z f") #'do-something)

I suppose I could use (concat "<leader>" ,key) for evil. How does meow know if the
key is for normal or insert? If I'm using the leader key, normal mode would be assumed? This is what I
came up with:

(setq my/modal 'evil)

(defmacro my/define-key (state map &rest rest)

(pcase my/modal
 ('evil `(evil-define-key ,state ,map ,rest)) 
 ('meow  `(meow-leader-define-key '(,key . ,func ) ))
 (_ (display-warning :wrong-variable "Wrong variable set for my/modal"))))


(my/define-key 'normal global-map  "zz" (lambda () (interactive)(message "hi zz")) )

The current problem I have is getting arguments out of a list then passing them to evil
or meow. The macro will do this which is invalid.

(evil-define-key 'normal global-map ("zz" (lambda nil (interactive) (message "hi zz"))))

Would like this, then figure out how to implement it for meow.

(evil-define-key 'normal global-map "zz" (lambda nil (interactive) (message "hi zz")))

Thanks

@DogLooksGood
Copy link
Collaborator

DogLooksGood commented Mar 26, 2023

@rickalex21 I think I completely got your idea, but it sounds to be against the concept of this package.

Meow has only one leader, it doesn't matter it's in normal mode or motion mode. Use meow-leader-define-key to create a keybinding in current leader map without <leader>, but it's not recommended to have a complex leader system in meow since it's not reusable, and not accessible in insert state. I guess this package will be really weird when you are trying to duplicate the evil's experience.

Personally, I have few leader key bindings. All rest are default, and invoked by keypad.
https://github.com/DogLooksGood/meomacs/blob/master/private_template.org#leader

@rickalex21
Copy link

It looks like I will create a bunch of c-c and to use them with meow leader. That's the awesome thing about meow that you can reuse existing keys. Will have to look at that thanks.

@licht1stein
Copy link

licht1stein commented May 1, 2023

How do I define this: I want j and l in dired-mode to go in and out of directories in normal mode?

Like this snippet from system crafters:
image

@DogLooksGood
Copy link
Collaborator

@licht1stein Just bind them on dired-mode-map.

@licht1stein
Copy link

Makes me feel stupid :) thank you!

@zackattackz
Copy link

Hi I do a similar thing as here where I bind mode-specific key maps to be different functions than whats set in the global map. Everything works fine, the function I set gets run instead of the global one. However in the key pad the display name is still the global function, not the mode-local one that is actually going to be run. This is minor but was wondering if there was a fix?

@DogLooksGood
Copy link
Collaborator

@zackattackz Can you show a snippet about what you have done?

@zackattackz
Copy link

zackattackz commented May 11, 2023

Yes so I have the following for bindings on C-b

(global-unset-key (kbd "C-b"))
(global-set-key (kbd "C-b b") #'ivy-switch-buffer)
(global-set-key (kbd "C-b q") #'kill-current-buffer)
(global-set-key (kbd "C-b n") #'next-buffer)
(global-set-key (kbd "C-b p") #'previous-buffer)
(define-key git-commit-mode-map (kbd "C-b b") #'run-term) ; run-term is just here to test for now

Then in my meow-setup I have:

(setq meow-cheatsheet-layout meow-cheatsheet-layout-qwerty)
(meow-leader-define-key
  '("b" . "C-b")

On my scratch buffer pressing C-b gives:
image

And C-b b runs ivy-switch-buffer

Inside the git-commit-mode, pressing C-b gives:
image

Although it says C-b b is bound to ivy-switch-buffer, pressing b here will actually run run-term as intended.

EDIT:

I've reproduced the same issue with a minimal init.el:

(require 'package)

(setq package-archives '(("melpa" . "https://melpa.org/packages/")
                         ("org" . "https://orgmode.org/elpa/")
                         ("elpa" . "https://elpa.gnu.org/packages/")))
(package-initialize)
(unless package-archive-contents
 (package-refresh-contents))
;; Initialize use-package on non-Linux platforms
(unless (package-installed-p 'use-package)
   (package-install 'use-package))
(require 'use-package)
(setq use-package-always-ensure t)
(use-package magit)
(use-package meow)

(global-unset-key (kbd "C-b"))
(define-key git-commit-mode-map (kbd "C-b b") #'with-editor-cancel)
(global-set-key (kbd "C-b b") #'switch-to-buffer)

(defun meow-setup ()
  "Setup meow bindings."
  (setq meow-keypad-ctrl-meta-prefix ?z)
  (setq meow--kbd-delete-char "<deletechar>")
  (setq meow--kbd-backward-line "<up>")
  (setq meow--kbd-backward-char "<left>")
  (setq meow-cheatsheet-layout meow-cheatsheet-layout-qwerty)
  (meow-motion-overwrite-define-key
   '("j" . meow-next)
   '("k" . meow-prev)
   '("<escape>" . ignore))
  (meow-leader-define-key
       ;; SPC j/k will run the original command in MOTION state.
   '("j" . "H-j")
   '("k" . "H-k")
   ;; Use SPC (0-9) for digit arguments.
   '("1" . meow-digit-argument)
   '("2" . meow-digit-argument)
   '("3" . meow-digit-argument)
   '("4" . meow-digit-argument)
   '("5" . meow-digit-argument)
   '("6" . meow-digit-argument)
   '("7" . meow-digit-argument)
   '("8" . meow-digit-argument)
   '("9" . meow-digit-argument)
   '("0" . meow-digit-argument)
   '("/" . meow-keypad-describe-key)
   '("?" . meow-cheatsheet)
   '("<SPC>" . execute-extended-command)
   '("e" . save-buffer)
   '("b" . "C-b"))
  (meow-normal-define-key
   '("0" . meow-expand-0)
   '("9" . meow-expand-9)
   '("8" . meow-expand-8)
   '("7" . meow-expand-7)
   '("6" . meow-expand-6)
   '("5" . meow-expand-5)
   '("4" . meow-expand-4)
   '("3" . meow-expand-3)
   '("2" . meow-expand-2)
   '("1" . meow-expand-1)
   '("-" . negative-argument)
   '(";" . meow-reverse)
   '("," . meow-inner-of-thing)
   '("." . meow-bounds-of-thing)
   '("[" . meow-beginning-of-thing)
   '("]" . meow-end-of-thing)
   '("a" . meow-append)
   '("A" . meow-open-below)
   '("b" . meow-back-word)
   '("B" . meow-back-symbol)
   '("c" . meow-change)
   '("d" . meow-delete)
   '("D" . meow-backward-delete)
   '("e" . meow-next-word)
   '("E" . meow-next-symbol)
   '("f" . meow-find)
   '("g" . meow-cancel-selection)
   '("G" . meow-grab)
   '("h" . meow-left)
   '("H" . meow-left-expand)
   '("i" . meow-insert)
   '("I" . meow-open-above)
   '("j" . meow-next)
   '("J" . meow-next-expand)
   '("k" . meow-prev)
   '("K" . meow-prev-expand)
   '("l" . meow-right)
   '("L" . meow-right-expand)
   '("m" . meow-join)
   '("n" . meow-search)
   '("o" . meow-block)
   '("O" . meow-to-block)
   '("p" . meow-yank)
   '("Q" . meow-goto-line)
   '("r" . meow-replace)
   '("R" . meow-swap-grab)
   '("s" . meow-kill)
   '("t" . meow-till)
   '("u" . meow-undo)
   '("U" . meow-undo-in-selection)
   '("v" . meow-visit)
   '("w" . meow-mark-word)
   '("W" . meow-mark-symbol)
   '("x" . meow-line)
   '("X" . meow-goto-line)
   '("y" . meow-save)
   '("Y" . meow-sync-grab)
   '("z" . meow-pop-selection)
   '("'" . repeat)
   '("C-d" . View-scroll-half-page-forward)
   '("C-u" . View-scroll-half-page-backward)
   '("<escape>" . ignore)))

(meow-setup)
(meow-global-mode 1)
;(suppress-keymap mode-specific-map)


Am on emacs 27.1 btw

@zackattackz
Copy link

There is more in my configs that could possibly be affecting this but I shared only what I think is relevant. But if it can't be reproduced let me know and I can share full init.el. I don't expect you to go out of your way to solve my problem but I very much appreciate meow and I hope I can contribute back some day.

@DogLooksGood
Copy link
Collaborator

DogLooksGood commented May 12, 2023

@zackattackz Please check this

;; By default, the leader keymap is fixed as `mode-specific-map'
;; It can either be set to a keybinding or another keymap.
;; Since we want different keymaps for different major modes,
;; "C-c" is used as a general way to find the leader keymap.
;; According to Emacs keybinding convention, user custom keybindings
;; should locate under C-c <letter>.
(setq meow-keypad-leader-dispatch "C-c")

;; Create a default keymap
(defvar my-buffer-keymap
    (let ((keymap (make-keymap)))
      (define-key keymap "b" #'switch-to-buffer)
      (define-key keymap "q" #'kill-current-buffer)
      keymap))
;; Give it a name for better keypad dispaly
(defalias 'my-buffer-keymap my-buffer-keymap)

;; Give it a keybinding
(global-set-key (kbd "C-c B") 'my-buffer-keymap)

;; Create a mode specific keymap
(defvar my-elisp-buffer-keymap
  (let ((keymap (copy-keymap my-buffer-keymap)))
    (define-key keymap "x" #'last-buffer)
    keymap))

;; Give it a name for better keypad dispaly
(defalias 'my-elisp-buffer-keymap my-elisp-buffer-keymap)

;; Override the default keybinding in this major mode
(define-key emacs-lisp-mode-map (kbd "C-c B") 'my-elisp-buffer-keymap)

By this way, you can build your own mode specific keybindings all with builtins, and it's well supported by Meow.
However most packages already provide you nice keybindings, so I don't really do this a lot in my own case.

I also recommend you to keep all default keybindings unchanged, using C-b is a bad practice. Put your keybindings under C-c <letter> ..., and call them with SPC <letter> ....

@zackattackz
Copy link

zackattackz commented May 12, 2023

Ok thanks for the advice with using C-c, makes sense will use it going forward.

I attempted your suggestion and it seems that actually pressing C-c b b will have the intended effect based on buffer.

However using the meow keypad (<SPC> b b) will only ever display the global bindings and names, and it will only ever run the globally bound function.

I will paste what I did below in case it's an error on my end.

Thanks

(defvar my-buffer-keymap
  (let ((keymap (make-keymap)))
	(define-key keymap "b" #'ivy-switch-buffer)
	(define-key keymap "n" #'next-buffer)
	(define-key keymap "p" #'previous-buffer)
	(define-key keymap "q" #'kill-current-buffer)
    keymap))
(defalias 'my-buffer-keymap my-buffer-keymap)
(global-set-key (kbd "C-c b") 'my-buffer-keymap)

(defvar my-git-commit-mode-keymap
  (let ((keymap (copy-keymap my-buffer-keymap)))
    (define-key keymap "b" #'run-term)
    keymap))
(defalias 'my-git-commit-mode-keymap my-git-commit-mode-keymap)
(define-key git-commit-mode-map (kbd "C-c b") 'my-git-commit-mode-keymap)

I cleared any related entries in meow-leader-define-key

@DogLooksGood
Copy link
Collaborator

@zackattackz Guess you are missing (setq meow-keypad-leader-dispatch "C-c")?

@zackattackz
Copy link

Yup, good call.

Adding that fixes the functionality of the keypad so that the right command is running now, my bad.

But the keypad still reads "b -> ivy-switch-buffer" rather than run-term.

@zackattackz
Copy link

Interestingly the keypad does properly switch between "my-buffer-keymap" and "my-git-commit-mode-keymap" on the top level though

@zackattackz
Copy link

Maybe I am going about using meow wrongly though. My original intention is to try to have consistent binds across different modes, as well as for the keypad to guide me in case I forgot the exact key mappings. Similarly to how spacemacs works, but I didn't want to use spacemacs itself or evil mode.

Should I go about this a different way?

@DogLooksGood
Copy link
Collaborator

DogLooksGood commented May 12, 2023

I think it's a display bug, just had a fix with f6a1b48.

There's nothing right or wrong, you can have this mode specific keymap without any external packages, it's a just how Meow reuses it in leader system.

Instead of copy keymap, it's better use set-keymap-parent to extend.

(defvar my-buffer-keymap
  (let ((keymap (make-keymap)))
	(define-key keymap "b" #'ivy-switch-buffer)
	(define-key keymap "n" #'next-buffer)
	(define-key keymap "p" #'previous-buffer)
	(define-key keymap "q" #'kill-current-buffer)
    keymap))
(defalias 'my-buffer-keymap my-buffer-keymap)
(global-set-key (kbd "C-c b") 'my-buffer-keymap)

(defvar my-git-commit-mode-keymap
  (let ((keymap (make-sparse-keymap)))
    (set-keymap-parent my-buffer-keymap) ;;; <------------------ HERE
    (define-key keymap "b" #'run-term)
    keymap))
(defalias 'my-git-commit-mode-keymap my-git-commit-mode-keymap)
(define-key git-commit-mode-map (kbd "C-c b") 'my-git-commit-mode-keymap)

@zackattackz
Copy link

Thank you, works great!

@A7R7
Copy link

A7R7 commented Nov 13, 2023

I recently found a workaround to bind mode and meow-state specific keybinds, without hacking into meow. You may check this discussion

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

Successfully merging this pull request may close these issues.

7 participants