Skip to content

Instantly share code, notes, and snippets.

@progfolio
Last active March 20, 2024 17:04
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save progfolio/1c96a67fcec7584b31507ef664de36cc to your computer and use it in GitHub Desktop.
Save progfolio/1c96a67fcec7584b31507ef664de36cc to your computer and use it in GitHub Desktop.
Spacemacs-like menus using general.el

Spacemacs-like menus using general.el

Global keybindings

First, we define a global prefix key:

(general-create-definer global-definer
  :keymaps 'override
  :states  '(insert emacs normal hybrid motion visual operator)
  :prefix  "SPC"
  :non-normal-prefix "S-SPC")

Bind top level, global commands like so:

(global-definer
  "!"   'shell-command
  ":"   'eval-expression)

Nested menus

To ease the creation of nested menus, we define a macro:

(defmacro +general-global-menu! (name infix-key &rest body)
  "Create a definer named +general-global-NAME wrapping global-definer.
Create prefix map: +general-global-NAME. Prefix bindings in BODY with INFIX-KEY."
  (declare (indent 2))
  `(progn
     (general-create-definer ,(intern (concat "+general-global-" name))
       :wrapping global-definer
       :prefix-map (quote ,(intern (concat "+general-global-" name "-map")))
       :infix ,infix-key
       :wk-full-keys nil
       "" '(:ignore t :which-key ,name))
     (,(intern (concat "+general-global-" name))
      ,@body)))

This macro defines top-level nested menus. e.g. a menu for “buffer” bindings:

(+general-global-menu! "buffer" "b"
  "d"  'kill-current-buffer
  "o" '((lambda () (interactive) (switch-to-buffer nil))
        :which-key "other-buffer")
  "p"  'previous-buffer
  "r"  'rename-buffer
  "M" '((lambda () (interactive) (switch-to-buffer "*Messages*"))
        :which-key "messages-buffer")
  "n"  'next-buffer
  "s" '((lambda () (interactive) (switch-to-buffer "*scratch*"))
        :which-key "scratch-buffer")
  "TAB" '((lambda () (interactive) (switch-to-buffer nil))
          :which-key "other-buffer"))

You can specify per-package keybindings with the :general use-package keyword. For example, here’s a minimal helm configuration that binds SPC bb to helm-mini:

(use-package helm
  :init (require 'helm-config)
  :defer 1
  :config
  (helm-mode)

  :general
  (+general-global-buffer
    "b" 'helm-mini))

Contextual (mode specific) leader key

We define a global-leader definer to access major-mode specific bindings:

(general-create-definer global-leader
  :keymaps 'override
  :states '(emacs normal hybrid motion visual operator)
  :prefix "SPC m"
  "" '(:ignore t :which-key (lambda (arg) `(,(cadr (split-string (car arg) " ")) . ,(replace-regexp-in-string "-mode$" "" (symbol-name major-mode))))))

Again, we use the :general keyword to bind mode-specific bindings. For example, with the following, we can eval-buffer when in emacs-lisp-mode or lisp-interaction-mode by pressing SPC m e b:

(use-package elisp-mode
  ;;this is a built in package, so we don't want to try and install it
  :ensure nil
  :general
  (global-leader
    ;;specify the major modes these should apply to:
    :major-modes
    '(emacs-lisp-mode lisp-interaction-mode t)
    ;;and the keymaps:
    :keymaps
    '(emacs-lisp-mode-map lisp-interaction-mode-map)
    "e" '(:ignore t :which-key "eval")
    "eb" 'eval-buffer
    "ed" 'eval-defun
    "ee" 'eval-expression
    "ep" 'pp-eval-last-sexp
    "es" 'eval-last-sexp
    "i" 'elisp-index-search))

Conclusion

Hope this helps. ~ Nicholas Vollmer

@mathisto
Copy link

mathisto commented Oct 6, 2021

Hail Nicholas! I just wanted to say thanks for sharing this. I'm very new to the Emacs worlds and was looking for just this type of DSL for defining mnemonic trees.

@progfolio
Copy link
Author

You're welcome. If you're looking for more usage examples, I still use this is in my init:

https://github.com/progfolio/.emacs.d

@hongyi-zhao
Copy link

You told me the following here:

You still need to define +general-global-application as shown in the gist I linked earlier.
But this is beyond the scope of the original issue.

But I've already used the following code snippet in my use-package configuration:

(+general-global-application
   "t" '(:ignore t :which-key "terminal")
   "tt" 'vterm-other-window
   "t." 'vterm)

More specifically, I really don't know how to define +general-global-application. Any more hints will be highly appreciated.

@hongyi-zhao
Copy link

Do you mean I need to add something like the following?

(+general-global-menu! "application" "a"
[...]
)

@progfolio
Copy link
Author

correct

@hongyi-zhao
Copy link

@progfolio Thank you for your confirmation. Here, I found the corresponding configuration line you used, as shown below:

(+general-global-menu! "application" "a")

@hongyi-zhao
Copy link

hongyi-zhao commented Dec 1, 2021

A very strange thing: I can't open your commit, as shown below:

image

@progfolio
Copy link
Author

I provided the answer to that question before you opened that issue here:

radian-software/straight.el#891 (comment)

I disabled issues on my personal config. I'm interested in sharing it so that others may take inspiration from it, but I'm not generally interested in providing support for it.

@hongyi-zhao
Copy link

hongyi-zhao commented Dec 1, 2021

But I still can't find any hint/clue given by you on whether the syntax of the code line <<general-config>> used in your general configuration is correct or not, i.e., the one appearing in the following code snippets:

(use-package general
  :demand t
  :config
  (general-override-mode)
  (general-auto-unbind-keys)
  <<general-config>>)

@progfolio
Copy link
Author

progfolio commented Dec 1, 2021

From the comment I linked:

There may be some configuration context missing, etc. For example, the last test you've posted contains Org noweb syntax:

<<general-config>>

This is not valid elisp. It's a syntax used in Org src blocks which only results in a valid elisp file after tangling.

@hongyi-zhao
Copy link

hongyi-zhao commented Dec 1, 2021

So this line should be deleted from the testing configuration used by me. I've taken it for granted that all the stuff in your linked comment is generated by yodel automatically, which makes me lost the opportunity to check its content further exhaustively.

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