My personal Emacs configuration
This is not a starter kit, but maybe someone will find it useful, just as I have found othersโ configuration files useful, in particular, and not exclusively:
- Prelude
- rajeep/emacs
- Magnars/.emacs.d
- ohai-emacs
- technomancy/better-defaults
- Sacha Chuaโs .emacs.d
- Bozhidar Batsovโs .emacs.d
- gapar/.emacs.d
Most of the configurations are in this .org file, which is loaded in init.el
I use Emacs Plus. On macOS this can be installed through:
brew tap d12frosted/emacs-plus
brew install emacs-plus@30
Installed with the App Manager
brew install hunspell
in ~/Library/Spelling/ download and install the required dictionaries with
get http://cgit.freedesktop.org/libreoffice/dictionaries/plain/en/en_GB.aff
get http://cgit.freedesktop.org/libreoffice/dictionaries/plain/en/en_GB.dic
Running (getenv "LANG") on my system shows that the installed language is en_GB so there is no need to create symlinks to the directory as some web guides have suggested.
Installed with the App Manager
In the dotfiles repo there is a file hunspell_en_GB. Create a link to this with ln -s dotfiles/hunspell_en_GB ~/.hunspell_en_GB
Clone this repo to .emacs.d
git clone https://github.com/JungleCandy/ygg-emacs.git .emacs.d
Iโm using Neotree with icons, this requires M-x all-the-icons-install-fonts to be run.
If using Org-roam, make sure the directory structure is set up as defined, and pull down the content.
GPL v3, obviously
Doing this in Org-Mode As a better way to be organised. And while I am looking at it I can try and figure out what I know and what I need to know about
- Flyspell
- Company
- abbrev and family
- Projectile
- Org Mode
Configure what is built in.
(setq user-full-name "Abizer Nasir"
user-mail-address "abizern@mac.com"
user-domain "abizern.dev") ;; Reduce the frequency of garbage collection
(setq gc-cons-threshold 50000000)
;; Warn when opening very large files about 100MB
(setq large-file-warning-threshold 100000000)
;; We don't need a startup message.
(setq inhibit-startup-message t)
;; disable the annoying bell ring
(setq ring-bell-function 'ignore)
;; A suitably wide fill-column
(set-default 'fill-column 140)
;; Show column and line number in the modeline
(setq line-number-mode t)
(setq column-number-mode t)
;; Turn off modes that look ugly.
(mapc
(lambda (mode)
(when (fboundp mode)
(funcall mode -1)))
'(menu-bar-mode tool-bar-mode scroll-bar-mode horizontal-scroll-bar-mode))
;; more useful frame title, that show either a file or a
;; buffer name (if the buffer isn't visiting a file)
(setq frame-title-format
'((:eval (if (buffer-file-name)
(abbreviate-file-name (buffer-file-name))
"%b"))))
;; Tab-bar-mode
(tab-bar-mode 1)
(setq tab-bar-show 1)
;; Always load the newest version of a file, prevents stale compiled elisp code
(setq load-prefer-newer t)
;; Tab indentation is a curse, a historical pestilence.
;; Turn it off and let's never talk about this default again.
(set-default 'indent-tabs-mode nil)
;; Move files to trash
(setq delete-by-moving-to-trash t)
;; Automatically save buffers before launching M-x compile and friends,
;; instead of asking you if you want to save.
(setq compilation-ask-about-save nil)
;; Make the selection work like most people expect.
(delete-selection-mode t)
(transient-mark-mode t)
;; Automatically update unmodified buffers whose files have changed.
(global-auto-revert-mode t)
;; We aren't using monospace typewriters anymore
(setq sentence-end-double-space nil)
;; Since ethan-wspace takes care of this for us, we don't need it
(setq mode-require-final-newline nil)
(setq require-final-newline nil)
;; Turn off defadvice warnings during startup
(setq ad-redefinition-action 'accept)
;; use hippie-expand instead of dabbrev
(global-set-key (kbd "M-/") 'hippie-expand)
;; Always indent after a newline
(define-key global-map (kbd "RET") 'newline-and-indent)
;; A quick major mode help with discover-my-major
(define-key 'help-command (kbd "C-m") 'discover-my-major)
;; Align your code in a pretty way.
(global-set-key (kbd "C-x \\") 'align-regexp)
;; Ask for y/n confirmation instead of yes/no
(fset 'yes-or-no-p 'y-or-n-p)
;; Winner mode for layout persistence
(add-hook 'after-init-hook #'winner-mode)
;; Make sure to always use UTF-8
(setq locale-coding-system 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)
(set-selection-coding-system 'utf-8)
(prefer-coding-system 'utf-8)
;; Make eshell work nicely with zsh
(setq shell-file-name "/bin/zsh")
(setq explicit-zsh-args '("--interactive" "--login"))
(setq comint-process-echoes 0)
;; prevents echoes of commands in shell
(add-hook 'shell-mode-hook
(lambda () (setq comint-process-echoes t)))Allow switching between the Dracula Pro theme and the Tron Legacy theme
I like a bar cursor, so that that up in advance.
(setq-default cursor-type 'bar)First, create a convenience function to setup the cursor I like with Dracula Pro theme ,#+begin_src emacs-lisp (defun ygg/setup-dracula-cursor () โSet a gold bar cursor for Dracula Pro.โ (when (display-graphic-p) (set-face-attribute โcursor nil :background โgold1โ)))
Add the custom theme load path to the list
(add-to-list 'custom-theme-load-path
(expand-file-name "themes" user-emacs-directory))Create a global var as the theme:
(defvar ygg/theme 'tron
"Theme to load at startup.
Possible values are 'dracula or 'tron. Default is 'tron.")
(use-package tron-legacy-theme
:ensure t
:config
(setq tron-legacy-theme-vivid-cursor t))Load the chosen theme
(cond
((eq ygg/theme 'dracula)
(load-theme 'dracula-pro-pro :no-confirm)
(ygg/setup-dracula-cursor))
((eq ygg/theme 'tron)
(require 'tron-legacy-theme)
(load-theme 'tron-legacy t)))Make sure the correct cursor is loaded depending on the chosen theme
(add-hook 'after-make-frame-functions
(lambda (frame)
(with-selected-frame frame
(when (eq ygg/theme 'dracula)
(ygg/setup-dracula-cursor)))))Make the choice of theme interactive
(defun ygg/set-theme (theme)
"Disable the current theme and load THEME."
(interactive
(list (intern (completing-read "Theme: " '(dracula tron) nil t))))
(mapc #'disable-theme custom-enabled-themes)
(setq ygg/theme theme)
(pcase theme
('dracula
(load-theme 'dracula-pro-pro t)
(ygg/setup-dracula-cursor))
('tron
(require 'tron-legacy-theme)
(load-theme 'tron-legacy t))))
;; Directory for support files. Create if needed.
(defvar savefile-dir (expand-file-name "savefile" user-emacs-directory)
"The directory that stores support files.")
(unless (file-exists-p savefile-dir)
(make-directory savefile-dir))
;; Define where to keep the autoload declarations.
(setq autoload-file (expand-file-name "loaddefs.el" savefile-dir))
;; Define where to keep user-settings, and load them.
(setq custom-file (expand-file-name "custom.el" savefile-dir))
(load custom-file 'noerror)
;; User lisp files. Create if needed.
(defvar ygg-lisp-dir (expand-file-name "lisp" user-emacs-directory)
"The directory for user lisp files.")
(unless (file-exists-p ygg-lisp-dir)
(make-directory ygg-lisp-dir))
;; Add the user-lisp directory to the load path.
(add-to-list 'load-path ygg-lisp-dir)
;; store all backup and autosave files in the tmp dir
(setq backup-directory-alist
`((".*" . ,temporary-file-directory)))
(setq auto-save-file-name-transforms
`((".*" ,temporary-file-directory t)))Better buffer names if they clash
(require 'uniquify)
(setq
uniquify-buffer-name-style 'forward
uniquify-separator "/"
uniquify-after-kill-buffer-p t ;; rename after killing a buffer
uniquify-ignore-buffers-re "^\\*") ;; ignore special buffers;; Update package metadata if required
(unless package-archive-contents
(package-refresh-contents))
(unless (package-installed-p 'use-package)
(package-install 'use-package))
(require 'use-package)
(setq use-package-always-ensure t)
;; For more verbose startup, uncomment the line below
;; (setq use-package-verbose t) Donโt clutter the modeline with global minor modes
(use-package diminish
:ensure t)Easily move between windows, optimised for Dvorak layout.
| M-o | Put up indicators to make moving between windows easier |
| C-x C-o | Swap windows |
| C-u M-o | Swaps current window with selected window |
| C-u C-u M-o | Deletes the selected window |
(use-package ace-window
:ensure t
:bind (("M-o" . ace-window)
("C-x C-o" . ace-swap-window))
:config
(setq aw-keys '(?a ?o ?e ?u ?i ?d ?h ?t ?n))) Quick navigation by word or character
| C-; | avy-goto-word-1 |
| C-: | avy-goto-char |
(use-package avy
:ensure t
:bind (("C-;" . avy-goto-word-1)
("C-:" . avy-goto-char)))All good IDEs have some interactivity
(use-package company
:init (add-hook 'after-init-hook #'global-company-mode)
:commands company-mode
:config
;; Enable company-mode globally.
(global-company-mode +1)
;; Except when you're in term-mode.
(setq company-global-modes '(not term-mode))
;; Give Company a decent default configuration.
(setq company-minimum-prefix-length 2
company-selection-wrap-around t
company-show-numbers t
company-tooltip-align-annotations t
company-require-match nil
company-dabbrev-downcase nil
company-dabbrev-ignore-case nil)
;; Sort completion candidates that already occur in the current
;; buffer at the top of the candidate list.
(setq company-transformers '(company-sort-by-occurrence))
;; Show documentation where available for selected completion
;; after a short delay.
(use-package company-quickhelp
:ensure t
:after (company)
:config
(setq company-quickhelp-delay 1)
(company-quickhelp-mode 1))
;; Use C-\ to activate the Company autocompleter.
;; We invoke company-try-hard to gather completion candidates from multiple
;; sources if the active source isn't being very forthcoming.
(use-package company-try-hard
:ensure t
:after (company)
:bind ("C-\\" . company-try-hard)
:config
(bind-keys :map company-active-map
("C-\\" . company-try-hard)))
:diminish company-mode) (use-package eshell
:ensure t)(use-package eshell-git-prompt
:after shell
:ensure t)(use-package eshell-syntax-highlighting
:ensure t
:config
(eshell-syntax-highlighting-global-mode +1)
:init
(defface eshell-syntax-highlighting-invalid-face
'((t :inherit diff-error))
"Face used for invalid Eshell commands."
:group 'eshell-syntax-highlighting))See more at https://github.com/glasserc/ethan-wspace
| C-c c | to clean up a file |
(use-package ethan-wspace
:ensure t
:commands global-ethan-wspace-mode
:config (setq mode-require-final-newline nil)
(global-ethan-wspace-mode 1)
:bind ("C-c c" . ethan-wspace-clean-all)
:diminish ethan-wspace-mode) Select successively larger logical units. Works really well with multiple-cursors
| C-= | Select and expand by logical units |
| M-C-= | Contract the region be logical units |
(use-package expand-region
:ensure t
:bind (("C-=" . er/expand-region)
("M-C-=" . er/contract-region)))| C-s, C-r | Search in project |
| M-x | Run command |
| C-x C-f | Open File |
| C-x b | Switch buffer |
| C-c k | Search project with ripgrep |
| C-c g | Counsel-git - find git tracked file |
| C-C r | Open recent files list |
;; Ivy: lightweight completion
(use-package ivy
:diminish
:init
(ivy-mode 1)
:custom
(ivy-use-virtual-buffers t)
(ivy-count-format "(%d/%d) ") ;; Show current/total count
(enable-recursive-minibuffers t)
(ivy-initial-inputs-alist nil)) ;; Remove '^' from certain prompts
;; Swiper: better search
(use-package swiper
:bind (("C-s" . swiper)
("C-r" . swiper))) ;; Optional: replace isearch backward
;; Counsel: ivy-enhanced commands
(use-package counsel
:diminish
:after ivy
:bind (("M-x" . counsel-M-x)
("C-x C-m" . counsel-M-x)
("C-x C-f" . counsel-find-file)
("C-x b" . counsel-ibuffer)
("C-c k" . counsel-rg)
("C-c g" . counsel-git)
("C-c r" . counsel-recentf))
:config
(counsel-mode 1))
;; Nicer display
(use-package ivy-rich
:after ivy
:init
(ivy-rich-mode 1))For working in projects, integrated with Ivy, Swiper and Counsel
| C-c p f | Find file in current project |
| C-c p s | Search project with ripgrep |
| C-c p g | Grop |
| C-c p p | Switch project |
| C-c p x | Find references |
(use-package projectile
:diminish projectile-mode
:init (define-key global-map (kbd "C-c p") 'projectile-command-map)
:config
(projectile-mode 1)
:custom
(projectile-completion-system 'ivy)
(projectile-project-search-path '("~/Developer" "~/Developer/Tools for building" "~/Sites")) ; No depth restriction
(projectile-sort-order 'recentf))| Open Magit status | C-x g |
| Refresh status | g |
| Cycle sections | TAB / S-TAB |
| Next / prev section | n / p |
| Visit file / commit | RET |
| Stage file / hunk | s |
| Unstage file / hunk | u |
| Commit | c c |
| Amend last commit | c a |
| Push | P P |
| Pull / Fetch | F F |
| Checkout branch | b b |
| Create branch | b c |
| Merge | m m |
| Rebase | r r |
| Delete branch | b k |
| Rename branch | b m |
| Forge dispatch menu | # (from magit-status) |
| List issues | # i |
| List pull requests | # p |
| Create issue | M-x forge-create-issue |
| Create pull request | M-x forge-create-pullreq |
| Visit PR for current branch | M-x forge-visit-pullreq |
| Fetch issues / PRs (sync) | M-x forge-pull |
| Checkout PR branch | b y / M-x forge-checkout-pullreq |
| Add / send comment | C-c C-c |
| Reply to comment | r (on comment) |
| Add emoji reaction | : |
| View discussion timeline | RET (on issue/PR) |
| Toggle fine-grained diff | d |
| Edit patch inline | e |
| Resolve merge conflict | s +C-c ^ (or Ediff) |
| Quit Magit & restore layout | q (winner-undo) |
| magit-status | Open status for current repo |
| forge-browse-issues | Show issues in Forge buffer |
| forge-pull | Sync issues & PRs from remote forge |
| forge-push | Push issue / PR updates |
(defun my-magit-quit-session ()
"Quit Magit and restore previous window configuration."
(interactive)
(kill-buffer)
(winner-undo))
(use-package magit
:commands (magit-status)
:bind (("C-x g" . magit-status))
:config
;; Make magit-status open in the same window (like fullscreen)
(setq magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
;; Bind `q` to your custom quit in magit-status
(define-key magit-status-mode-map (kbd "q") #'my-magit-quit-session))
;; Force git-commit-mode to load early for commit messages via emacsclient
(require 'git-commit)
(add-to-list 'auto-mode-alist '("COMMIT_EDITMSG\\'" . git-commit-mode))
(add-hook 'git-commit-mode-hook
(lambda ()
(setq fill-column 72)
(turn-on-auto-fill)))For GitHub integration - set up for an authenticated gh.
(use-package forge
:after magit)| Keybinding | Action |
|---|---|
| C-c g | Fuzzy find Git-tracked files |
| C-c l | Fuzzy search Git commit log |
;; Use counsel-git to find files tracked by Git
(global-set-key (kbd "C-c g") #'counsel-git)
;; Use counsel-git-log for fuzzy searching Git commits
(global-set-key (kbd "C-c l") #'counsel-git-log)Adds smarter sorting and global fuzzy matching for all Ivy interfaces
;; Optional: Prescient sorting for Ivy
(use-package ivy-prescient
:after counsel
:config
(ivy-prescient-mode 1)
(prescient-persist-mode 1)
(setq counsel-M-x-sort-function #'ivy-prescient-sort-function))
;; Optional: Enable fuzzy matching in Ivy
(setq ivy-re-builders-alist
'((swiper . ivy--regex-plus)
(t . ivy--regex-ignore-order)))Aids discoverability with all these shortcuts
(use-package which-key
:diminish
:config (which-key-mode))Show changes in a file
| C-c g | prefix key |
| C-c g = | diff-hl-goto-hunk |
| C-c g n | diff-hl-next-hunk |
| C-c g p | diff-hl-previous-hunk |
| C-c g r | diff-hl-revert-hunk |
(define-prefix-command 'my/diff-hl-prefix)
(global-set-key (kbd "C-c g") 'my/diff-hl-prefix)
(use-package diff-hl
:ensure t
:hook ((prog-mode . diff-hl-mode)
(text-mode . diff-hl-mode)
(magit-pre-refresh . diff-hl-magit-pre-refresh)
(magit-post-refresh . diff-hl-magit-post-refresh))
:bind (("C-c g =" . diff-hl-diff-goto-hunk)
("C-c g n" . diff-hl-next-hunk)
("C-c g p" . diff-hl-previous-hunk)
("C-c g r" . diff-hl-revert-hunk))
:config
(diff-hl-flydiff-mode 1)
(diff-hl-show-hunk-mouse-mode 1))Spell check prose and code.
| M-x ispell-word RET | `ispell-word` | then press `a` to accept & add word |
| C-c w | `ispell-word` | one-key shortcut |
;; Hunspell setup (shared across machines)
(setq ispell-program-name "hunspell"
ispell-dictionary "en_GB"
ispell-personal-dictionary "~/.hunspell_en_GB"
ispell-hunspell-dictionary-alist
'(("en_GB" "[A-Za-z]" "[^A-Za-z]" "[']" nil
("-d" "en_GB") nil utf-8)))
(use-package flyspell
:ensure nil ;; builtโin
:defer 2 ;; give Emacs 2s to start up before loading, prevents recursion during tangling
:hook ((text-mode . flyspell-mode)
(org-mode . flyspell-mode)
(markdown-mode . flyspell-mode)
(latex-mode . flyspell-mode)
(prog-mode . flyspell-prog-mode))
:bind (("C-c w" . ispell-word))
:config
;; red wavy underline for mis-spellings
(set-face-attribute 'flyspell-incorrect nil
:underline '(:style wave :color "red")))Move like a ninja if I could only remember the chords
| jj | avy-goto-word-1 | Jump forward by word |
| jl | avy-goto-line | Jump by line |
| jk | avy-goto-char | โkโ like navigation |
| jf | avy-goto-subword-0 | Fine grained symbol jumps |
| xx | counsel M-x | Easier to reach for than M-x |
| dk | docker | Help with docker |
| dy | docker-compose-helper-menu | For docker.yml |
(use-package key-chord
:ensure t
:custom
(key-chord-two-keys-delay 0.2) ;; adjust to your typing rhythm
:config
(key-chord-mode 1)
(key-chord-define-global "jj" 'avy-goto-word-1)
(key-chord-define-global "jl" 'avy-goto-line)
(key-chord-define-global "jk" 'avy-goto-char)
(key-chord-define-global "jf" 'avy-goto-subword-0)
(key-chord-define-global "xx" #'counsel-M-x)
(key-chord-define-global "dk" 'docker)
(key-chord-define-global "dy" 'docker-compose-helper-menu))Why edit one line when you can work on many
| C-> | mc/mark-next-like-this |
| C-< | mc/mark-previous-like-this |
| C-c C-c | mc/mark-all-like-this |
| C-S-c C-S-c | mc/edit-lines |
| C-S-c C-S-e | mc/edit-ends-of-lines |
| C-S-c C-S-a | mc/edit-beginnings-of-lines |
(use-package multiple-cursors
:ensure t
:commands multiple-cursors-mode
:bind (("C->" . mc/mark-next-like-this)
("C-<" . mc/mark-previous-like-this)
("C-c C-<" . mc/mark-all-like-this)
("C-S-c C-S-c" . mc/edit-lines)
("C-S-c C-S-e" . mc/edit-ends-of-lines)
("C-S-c C-S-a" . mc/edit-beginnings-of-lines))
:config
(setq mc/list-file (expand-file-name ".mc-lists.el" savefile-dir))) | <F5> | neotree-toggle |
Bindings only in Neotree buffer.
| n, p | next-line, previos-line |
| <SPC>, <RET>, <TAB> | Open current item if file, Toggle if directory |
| U | Go up a directory |
| g | Refresh |
| A | Toggle maximise window |
| H | Toggle display hidden files |
| Q | Recursively open a directory |
| C-c C-n | Create file (directory if filename ends with //) |
| C-c C-d | Delete file or directory |
| C-c C-r | Rename file or directory |
| C-c C-c | Change the root of the directory |
| C-c C-p | Copy a file or a directory |
(use-package neotree
:ensure t
:bind ("<f5>" . neotree-toggle)
:custom
(neo-theme 'icons)
(neo-smart-open t)
(neo-autorefresh t)
(neo-show-hidden-files t))
(use-package all-the-icons
:ensure t
:defer
:if (display-graphic-p))
(use-package all-the-icons-completion
:ensure t
:defer
:hook (marginalia-mode . #'all-the-icons-completion-marginalia-setup)
:init
(all-the-icons-completion-mode))Colourise names of colours in certain modes
(use-package rainbow-mode
:ensure t
:config
(dolist (mode '(css-mode less-css-mode html-mode web-mode))
(add-hook (intern (concat (symbol-name mode) "-hook"))
(lambda () (rainbow-mode))))
:diminish rainbow-mode) Recent File handling
(use-package recentf
:ensure t
:init
(setq recentf-save-file (expand-file-name "recentf" savefile-dir)
recentf-max-saved-items 100
recentf-max-menu-items 15
recentf-auto-cleanup 'never
recentf-exclude
'("\\COMMIT_EDITMSG\\'"
".*-autoloads\\.el\\'"
".*/elpa/.*"
"/tmp/"
"^/ssh:"
"^/sudo:"))
:config
(recentf-mode 1))Save history.
(use-package savehist
:config
(setq savehist-additional-variables
;; search entries
'(search-ring regexp-search-ring)
;; save every minute
savehist-autosave-interval 60
;; keep the home clean
savehist-file (expand-file-name "savehist" savefile-dir))
(savehist-mode +1)) Save point position between sessions.
(use-package saveplace
:ensure t
:init
(setq save-place-file (expand-file-name ".places" savefile-dir))
:config
(setq-default save-place t))Brackets are really, really important
| C-M-f | Move forward across one balanced expression |
| C-M-b | Move backward across one balanced expression |
| C-M-n | Move forward out of one level of parentheses |
| C-M-d | Move forward down one level of sexp |
| C-M-u | Move backward out of one level of parentheses |
| C-M-p | Move backward down one level of sexp |
| C-M-w | Copy the following ARG expressions to the kill-ring (sp-copy-sexp) |
| M-s | Unwrap the current list |
| M-r | Unwrap the list and kill everything inside expect the next expression |
| C-) | Slurp the following list into current by moving the closing delimiter |
| C-} | Remove the last sexp in the current list by moving the closing delimiter |
| C-( | Slurp the preceding sexp into the current one my moving the opening delimeter |
| C-{ | Barfs backwards |
| M-S | Split the list or string at point into two |
| M-J | Join the sexp before and after the point if they are of the same type |
| C-M-t | Transpose the expressions around the point |
(use-package smartparens
:diminish
:ensure t
:init
(require 'smartparens-config)
:hook
((emacs-lisp-mode . smartparens-strict-mode)
(lisp-mode . smartparens-strict-mode)
(sly-mrepl-mode . smartparens-strict-mode))
:config
(smartparens-global-mode t)
(show-smartparens-global-mode t)
(setq sp-highlight-pair-overlay nil
sp-highlight-wrap-overlay nil
sp-highlight-wrap-tag-overlay nil)
;; Avoid pairing backticks in elisp strings
(sp-local-pair 'emacs-lisp-mode "`" nil :when '(sp-in-string-p))
:bind
(("C-M-f" . sp-forward-sexp)
("C-M-b" . sp-backward-sexp)
("C-M-n" . sp-up-sexp)
("C-M-d" . sp-down-sexp)
("C-M-u" . sp-backward-up-sexp)
("C-M-p" . sp-backward-down-sexp)
("C-M-w" . sp-copy-sexp)
("M-s" . sp-splice-sexp)
("M-r" . sp-splice-sexp-killing-around)
("C-)" . sp-forward-slurp-sexp)
("C-}" . sp-forward-barf-sexp)
("C-(" . sp-backward-slurp-sexp)
("C-{" . sp-backward-barf-sexp)
("M-S" . sp-split-sexp)
("M-J" . sp-join-sexp)
("C-M-t" . sp-transpose-sexp)))Automatically save files
(use-package super-save
:diminish
:ensure t
:config
(super-save-mode +1)) A little simpler than undo tree
| C-z | Undo |
| C-S-z | Redo |
(use-package undo-fu
:ensure t
:config
(global-unset-key (kbd "C-z"))
(global-set-key (kbd "C-z") 'undo-fu-only-undo)
(global-set-key (kbd "C-S-z") 'undo-fu-only-redo)) A better version of zap-to-char.
(use-package zop-to-char
:ensure t
:bind
(("M-z" . zop-up-to-char)
("M-Z" . zop-to-char))) (use-package f
:ensure t)Mostly the mode hooks and a couple of keybindings
| M-n | Add line below |
| M-p | Add line above |
(use-package markdown-mode
:ensure t
:mode ("\\.md\\'" "\\.markdown\\'")
:hook ((markdown-mode . visual-line-mode)
(markdown-mode . spell-fu-mode))
:bind (:map markdown-mode-map
("M-n" . open-line-below)
("M-p" . open-line-above)))(use-package tex
:ensure auctex
:hook ((LaTeX-mode . visual-line-mode)
(LaTeX-mode . spell-fu-mode)
(LaTeX-mode . LaTeX-math-mode)
(LaTeX-mode . turn-on-reftex))
:custom
(TeX-auto-save t)
(TeX-parse-self t)
(TeX-save-query nil)
(TeX-PDF-mode t)
(TeX-source-correlate-mode t)
(TeX-source-correlate-start-server t)
;; Default to latexmk
(TeX-command-default "LatexMk")
(TeX-command-list
'(("LatexMk" "latexmk -pdf -interaction=nonstopmode -synctex=1 %s"
TeX-run-TeX nil t
:help "Run LatexMk")))
;; View PDFs with PDF Tools if available
(TeX-view-program-selection
'((output-pdf "PDF Tools"))));; Compile LaTeX to PDF by default
(setq TeX-PDF-mode t)
;; Set path to epdfinfo dynamically (important for macOS)
(let ((default-directory "~/.emacs.d/elpa/"))
(setq pdf-info-epdfinfo-program
(expand-file-name
"epdfinfo"
(car (directory-files default-directory t "pdf-tools.*" t)))))
;; Auto-revert PDF buffer after LaTeX compilation
(use-package pdf-tools
:ensure t
:config
(pdf-tools-install)
(add-hook 'TeX-after-compilation-finished-functions
#'TeX-revert-document-buffer))Support for using Org mode for writing, blogging and note-taking.
(use-package org
:ensure t
:after yasnippet
:hook ((org-mode . visual-line-mode)
(org-mode . yas-minor-mode))
:bind (:map org-mode-map
("M-j" . org-metaup)
("M-k" . org-metadown)
("C-c t" . yas-next-field))
:config
(setq org-directory "~/Documents/Org"
org-metadir (concat org-directory "_orgmata/")
org-default-notes-file (concat org-directory "refile.org")
org-archive-location (concat org-metadir "archive.org::date-tree")
org-agenda-files '("~/Documents/Org/")
org-startup-indented t
org-src-tab-acts-natively t
org-src-fontify-natively t
org-src-preserve-indentation t
org-edit-src-content-indentation 0))
(defun ygg/fix-org-tab-width ()
"Ensure tab-width is 8 in Org mode buffers to satisfy parser requirements."
(setq-local tab-width 8))
(add-hook 'org-mode-hook #'ygg/fix-org-tab-width)| C-c ! | Insert an inactive timestamp (between square brackets) default behaviour |
| C-c f | Flycheck menu command prefix |
(with-eval-after-load 'flycheck
(define-key flycheck-mode-map (kbd "C-c !") nil)) ;; remove prefix map
(with-eval-after-load 'org
(define-key org-mode-map (kbd "C-c !") #'org-time-stamp-inactive))
(with-eval-after-load 'flycheck
(define-key flycheck-mode-map (kbd "C-c f") flycheck-command-map))(use-package org-bullets
:ensure t
:hook (org-mode . org-bullets-mode))
(setq org-pretty-entities t)(use-package org-cliplink
:ensure t
:after org
:bind (:map org-mode-map
("C-c M-l" . org-cliplink)))Build a cross-platform โsecond brainโ (macOS + Linux) backed by a Git repo.
- Repo root: ~/org-roam/
- Notes: ~/org-roam/notes/
- Dailies: ~/org-roam/daily/
- Database: ~/org-roam/org-roam.db (gitignored; rebuilt locally)
Key bindings:
| Key | Command | What it does |
|---|---|---|
| C-c n f | org-roam-node-find | Find a node; creates it if it doesnโt exist. |
| C-c n i | org-roam-node-insert | Insert a link to a node at point (create if needed). |
| C-c n c | org-roam-capture | Create a new node using capture templates. |
| C-c n l | org-roam-buffer-toggle | Toggle backlinks/side buffer for the current node. |
| C-c n d | org-roam-dailies-capture-today | Open/create todayโs daily note. |
Troubleshooting:
- M-x org-roam-db-sync (rebuild DB)
- M-x org-roam-db-clear-all then org-roam-db-sync (full reset)
(use-package org-roam
:ensure t
:after org
:init
;; Paths
(setq org-roam-repo-root
(file-truename (expand-file-name "~/org-roam/")))
(setq org-roam-directory
(file-truename (expand-file-name "notes" org-roam-repo-root)))
(setq org-roam-dailies-directory
(file-truename (expand-file-name "daily" org-roam-repo-root)))
(setq org-roam-db-location
(file-truename (expand-file-name "org-roam.db" org-roam-repo-root)))
:bind (("C-c n l" . org-roam-buffer-toggle)
("C-c n f" . org-roam-node-find)
("C-c n i" . org-roam-node-insert)
("C-c n c" . org-roam-capture)
("C-c n d" . org-roam-dailies-capture-today))
:config
(org-roam-db-autosync-mode)
;; Node capture template
(setq org-roam-capture-templates
'(("d" "default" plain "%?"
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
"#+title: ${title}\n#+date: %U\n\n")
:unnarrowed t)))
;; Daily capture template
(setq org-roam-dailies-capture-templates
'(("d" "default" entry "* %?"
:if-new (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n\n")))))
For nice visualisation other than using backlinks
(use-package org-roam-ui
:ensure t
:after org-roam
:config
(setq org-roam-ui-sync-theme t
org-roam-ui-follow t
org-roam-ui-update-on-save t
org-roam-ui-open-on-start t))Not sure if I actually use this, but leaving it in for now.
(with-eval-after-load 'org
(setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "|" "DONE(d)")
(sequence "DRAFT(r)" "|" "PUBLISH(p)"))
org-use-fast-todo-selection t
org-log-done 'time
org-treat-S-cursor-todo-selection-as-state-change nil))(with-eval-after-load 'ox-latex
(add-to-list 'org-latex-classes
'("article"
"\\documentclass[a4paper]{scrartcl}
\\usepackage[utf8]{inputenc}
\\usepackage{amsmath}
\\usepackage{amssymb}
\\usepackage{fullpage}"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
(add-to-list 'org-latex-classes
'("tufte-handout"
"\\documentclass[a4paper]{tufte-handout}
\\usepackage[utf8]{inputenc}
\\usepackage{amsmath}
\\usepackage{amssymb}"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}"))))(use-package ox-hugo
:ensure t
:after ox)Spaced Repetitive learning.
| org-drill | Start a drill |
| q | Finish the drill early |
| s | Skip the current item |
| e | Escape the drill and edit the current buffer |
| org-drill-resume | Continue the drill after editing |
Result of recall can be scored:
| 0 | Wrong, answer unfamiliar |
| 1 | Wrong answer |
| 2 | Almost, but not quite correct |
| 3 | Correct answer with some effort |
| 4 | Correct answer with a little thought |
| 5 | Correct answer, effortless |
| ? | Show help for these options |
(use-package org-drill
:ensure t
:config
(add-to-list 'org-modules 'org-drill)
(setq org-drill-add-random-noise-to-intervals-p t
org-drill-learn-fraction 0.25))(global-set-key (kbd "C-c l") #'org-store-link)
(global-set-key (kbd "C-c a") #'org-agenda)
(global-set-key (kbd "C-c c") #'org-capture)Be more explicit about layout
(use-package editorconfig
:ensure t
:config (editorconfig-mode +1))Apply styles to emacs modes that donโt pick these up automatically
(defun ygg/apply-editorconfig-style ()
"Mimic .editorconfig indentation settings for various major modes."
(cond
;; Swift โ 2-space soft indent
((derived-mode-p 'swift-mode)
(setq indent-tabs-mode nil
tab-width 2
swift-indent-offset 2))
;; C/C++/ObjC โ 2-space soft indent
((derived-mode-p 'c-mode 'c++-mode 'objc-mode)
(setq indent-tabs-mode nil
tab-width 2
c-basic-offset 2))
;; Python โ PEP 8
((derived-mode-p 'python-mode)
(setq indent-tabs-mode nil
tab-width 4
python-indent-offset 4))
;; Makefile โ tabs required
((derived-mode-p 'makefile-gmake-mode)
(setq indent-tabs-mode t
tab-width 8))
;; Org โ strict 8-wide tabs
((derived-mode-p 'org-mode)
(setq indent-tabs-mode nil
tab-width 8))
;; Markdown / text โ allow spacey editing, usually 2
((derived-mode-p 'markdown-mode 'text-mode)
(setq indent-tabs-mode nil
tab-width 2))))
(add-hook 'org-mode-hook #'ygg/apply-editorconfig-style)
(add-hook 'org-src-mode-hook #'ygg/apply-editorconfig-style)
(use-package yasnippet
:ensure t
:init
(progn
(add-hook 'after-save-hook
(lambda ()
(when (eql major-mode 'snippet-mode)
(yas-reload-all))))
(setq yas-snippet-dirs (list (f-expand "snippets" user-emacs-directory)))
(setq yas-indent-line 'auto)
(yas-global-mode 1))
:mode ("\\.yasnippet" . snippet-mode))(use-package flycheck
:ensure t
:init (global-flycheck-mode));; Global LSP configuration
(use-package lsp-mode
:ensure t
:hook ((swift-mode . lsp)
(go-mode . lsp)
(python-mode . lsp))
:commands lsp
:custom
(lsp-eldoc-enable-hover nil)) ;; Let lsp-ui handle hover
(use-package lsp-ui
:ensure t
:hook (lsp-mode . lsp-ui-mode)
:custom
(lsp-ui-doc-enable t)
(lsp-ui-doc-show-with-cursor t)
(lsp-ui-sideline-enable t))My primary language. I should be able to use it on Linux as well.
First, locate `sourcekit-lsp`:
(defun find-sourcekit-lsp ()
(or (executable-find "sourcekit-lsp")
(and (eq system-type 'darwin)
(string-trim (shell-command-to-string "xcrun -f sourcekit-lsp")))
"sourcekit-lsp"))Enable `swift-mode` and hook it up to LSP:
(use-package swift-mode
:ensure t
:custom
(swift-mode:basic-offset 2)
:mode "\\.swift\\'"
:interpreter "swift")
(use-package lsp-sourcekit
:ensure t
:after lsp-mode
:custom
(lsp-sourcekit-executable (find-sourcekit-lsp)))I use SLY not Slime
| M-h | sly-documentation-lookup |
(defun lisp-program-location ()
"Return the SBCL path based on OS."
(cond
((eq system-type 'darwin) "/opt/homebrew/bin/sbcl") ;; macOS
((eq system-type 'gnu/linux) "ros run") ;; Linux via Roswell
(t "sbcl"))) ;; fallback
(use-package sly
:ensure t
:init
(setq inferior-lisp-program (lisp-program-location))
:config
(define-key sly-prefix-map (kbd "M-h") 'sly-documentation-lookup))Portable Rust configuration for Emacs 30.2 and environments where Rust may or may not be installed.
Useful Emacs keybindings and commands for working with Rust, rust-analyzer, Cargo, and Eglot. Safe to keep in your config even on machines without Rust.
| Category | Command / Keybinding | What It Does |
|---|---|---|
| **Editing** | `TAB` / `C-M-i` | Trigger completion (via rust-analyzer) |
| `M-.` | Jump to definition | |
| `M-,` | Jump back | |
| `C-c C-d` | Show documentation / type info (Eglot) | |
| `C-c C-r` | Apply code action / fix (Eglot) | |
| `C-M-` | Indent region | |
| (auto) | Format on save (rustfmt) | |
| **Cargo** | `M-x cargo-build` | Run `cargo build` |
| `M-x cargo-run` | Run `cargo run` | |
| `M-x cargo-test` | Run `cargo test` | |
| `M-x cargo-clean` | Clean project | |
| `M-x cargo-check` | Type-check project (`cargo check`) | |
| **Eglot** | `M-x eglot` | Start Eglot manually |
| (auto) | Eglot starts automatically in Rust buffers | |
| `M-x eglot-code-actions` | Show code fixes / refactorings | |
| `M-x eglot-reconnect` | Restart Eglot | |
| `M-x eglot-shutdown` | Stop LSP server | |
| **Rust Tools** | `M-x rust-format-buffer` | Format buffer using rustfmt |
| (auto) | rustfmt runs on save | |
| **Navigation** | `M-.` | Jump to definition |
| `M-,` | Go back | |
| `C-s` / `C-r` | Search forward/backward | |
| **Help** | `C-h f` | Describe function |
| `C-h v` | Describe variable | |
| `C-h m` | Show mode help (very useful!) |
;;; ----------------------
;;; Rust (tree-sitter) support
;;; ----------------------
;; Prefer the tree-sitter Rust mode
(use-package rust-ts-mode
:ensure t
:mode ("\\.rs\\'" . rust-ts-mode)
:init
;; Use rustfmt on save (if available)
(setq rust-format-on-save t))
;; Start Eglot only if rust-analyzer is present
(defun my/rust-eglot-if-available ()
(when (executable-find "rust-analyzer")
(eglot-ensure)))
(use-package eglot
:ensure nil ;; Eglot is built into Emacs 30
:hook (rust-ts-mode . my/rust-eglot-if-available))
;; Enable cargo-minor-mode only if cargo is installed
(defun my/rust-cargo-if-available ()
(when (executable-find "cargo")
(cargo-minor-mode 1)))
(use-package cargo
:ensure t
:hook (rust-ts-mode . my/rust-cargo-if-available))Active with venv, once I figure out how to do that.
Run M-x python-activate and point it at the .venv directory, although there is now a hook to point to one at the root of a project.
(use-package pyvenv
:ensure t
:config
(pyvenv-mode 1))
(use-package blacken
:ensure t
:hook (python-mode . blacken-mode)
:custom (blacken-line-length 88))
(use-package py-isort
:ensure t
:hook (before-save . py-isort-before-save))
(defun ygg/auto-pyvenv-activate ()
"Auto-activate local .venv if present."
(let ((root (locate-dominating-file default-directory ".venv")))
(when root
(pyvenv-activate (expand-file-name ".venv" root)))))
(add-hook 'python-mode-hook #'ygg/auto-pyvenv-activate)
(use-package go-mode
:ensure t
:mode "\\.go\\'"
:hook ((before-save . gofmt-before-save)))| C-c C-r | Mark the tag weโre in and itโs pair for renaming |
(defun my-web-mode-hook ()
(setq web-mode-markup-indent-offset 2
web-mode-css-indent-offset 2
web-mode-code-indent-offset 2))
(use-package web-mode
:ensure t
:mode (;; Want to use web-mode for HTML, not default html-mode.
("\\.html?\\'" . web-mode)
;; Add some extensions as per web-mode docs
("\\.phtml\\'" . web-mode)
("\\.tpl\\.php\\'" . web-mode)
("\\.[agj]sp\\'" . web-mode)
("\\.erb\\'" . web-mode)
("\\.mustache\\'" . web-mode)
("\\.djhtml\\'" . web-mode))
:hook (web-mode . my-web-mode-hook)
:config
(setq-default web-mode-enable-auto-pairing t
web-mode-enable-auto-closing t
web-mode-enable-auto-quoting t
web-mode-enable-css-colorization t
web-mode-enable-comment-keywords t
web-mode-enable-current-column-highlight t)
(bind-keys :map web-mode-map
("C-c C-r" . 'mc/mark-sgml-tag-pair)))| C-c <tab> | Beautify |
(use-package json-mode
:ensure t
:mode "\\.json\\'"
:hook ((json-mode . spell-fu-mode)
(json-mode . smartparens-strict-mode))
:bind (:map json-mode-map
("C-c <tab>" . json-mode-beautify)))(use-package docker
:ensure t
:bind ("C-c d" . docker))(use-package dockerfile-mode
:ensure t
:mode "Dockerfile\\'")(use-package yaml-mode
:ensure t
:mode "\\.ya?ml\\'")| C-c D | Show docker transient menu |
(require 'transient)
(transient-define-prefix docker-transient-menu ()
"Docker & Docker Compose Menu"
["Docker"
("p" "ps" (lambda () (interactive) (shell-command "docker ps")))
("l" "logs" (lambda () (interactive) (shell-command "docker logs -f $(docker ps -lq)")))
("r" "restart last" (lambda () (interactive) (shell-command "docker restart $(docker ps -lq)")))
("s" "stop all" (lambda () (interactive) (shell-command "docker stop $(docker ps -q)")))
("c" "clean system" (lambda () (interactive) (shell-command "docker system prune -af --volumes")))]
["Compose"
("u" "compose up" (lambda () (interactive) (shell-command "docker-compose up -d")))
("d" "compose down" (lambda () (interactive) (shell-command "docker-compose down")))
("b" "compose build" (lambda () (interactive) (shell-command "docker-compose build")))
("P" "compose ps" (lambda () (interactive) (shell-command "docker-compose ps")))])
(global-set-key (kbd "C-c D") #'docker-transient-menu)| C-c C-d | docker-compose-helper-menu |
(require 'transient)
(transient-define-prefix docker-compose-helper-menu ()
"Docker Compose Utilities"
["YAML & Validation"
("v" "config validation" (lambda () (interactive)
(shell-command "docker-compose config")))
("y" "show resolved YAML" (lambda () (interactive)
(shell-command "docker-compose config --resolve-image-digests")))]
["Service Info"
("l" "list services" (lambda () (interactive)
(shell-command "docker-compose config --services")))
("i" "inspect service" (lambda ()
(interactive)
(let ((svc (read-shell-command "Service name: ")))
(shell-command (format "docker-compose config --service %s" svc)))))])
(global-set-key (kbd "C-c C-d") #'docker-compose-helper-menu)(org-babel-do-load-languages
'org-babel-load-languages
'((shell . t)))docker ps --format "{{.Names}}"| M-g M-g | Show line numbers temporarily and prompt for the line to move to |
(defun goto-line-with-feedback ()
"Show line numbers temporarily, while prompting for the line number input."
(interactive)
(unwind-protect
(progn
(display-line-numbers-mode 1)
(call-interactively 'goto-line))
(display-line-numbers-mode -1)))
;; Remaps goto-line so that line numbers are turned on only when needed. M-g M-g
(global-set-key [remap goto-line] 'goto-line-with-feedback) Pretty print JSON using the Python helper function
(defun json-format ()
"Reformats the JSON in the region for humans."
(interactive)
(save-excursion
(shell-command-on-region (mark) (point) "python -m json.tool" (buffer-name) t)))| C-c C-d | 13/4/2024 |
| C-u C-t C-d | 2024-04-13 |
| C-u C-u C-d C-d | Tuesday, April 13, 2024 |
| C-c C-t | ISO 8601 formatted date/time |
;; Insert Date
;; Usage
;; - `C-c C-d` -> 13/04/2024
;; - `C-u C-c C-d` -> 2024-04-13
;; - `C-u C-u C-d C-d` -> Tuesday, April 13, 2024
(defun ygg-insert-date (prefix)
"Insert the current date. With prefix-argument use ISO format. With two
prefix arguments, write out the day and month name"
(interactive "P")
(let ((format (cond
((not prefix) "%d/%m/%Y")
((equal prefix '(4)) "%F")
((equal prefix '(16)) "%A, %B %d, %Y")))
(system-time-locale "en_GB"))
(insert (format-time-string format))))
(defun ygg-insert-iso-date-time ()
"Insert the current date in ISO format for UTC"
(interactive)
(insert (format-time-string "%FT%T%z" nil "UTC")))
(global-set-key (kbd "C-c C-d") 'ygg-insert-date)
(global-set-key (kbd "C-c C-t") 'ygg-insert-iso-date-time)Wrapper for parentheses
(defun ygg/wrap-with (s)
"Create a wrapper function for smartparens using S."
`(lambda (&optional arg)
(interactive "P")
(sp-wrap-with-pair ,s))) Not the easiest on a compact keyboard, but still useful to have.
| M-S-] | Move line up |
| M-S-[ | Move line down |
;; Xcode binding to move line up
(defun ygg/move-line-up ()
"Move the current line up"
(interactive)
(transpose-lines 1)
(forward-line -2)
(indent-according-to-mode))
(global-set-key (kbd "M-s-]")
(lambda ()
(interactive)
(ygg/move-line-up)))
;; Xcode binding to move line down
(defun ygg/move-line-down ()
"Move the current line down"
(interactive)
(forward-line 1)
(transpose-lines 1)
(forward-line -1)
(indent-according-to-mode))
(global-set-key (kbd "M-s-[")
(lambda ()
(interactive)
(ygg/move-line-down))) ;; Move about more quickly
;; move about in steps of 5 with C-S insteard of just C-
(global-set-key (kbd "C-S-n")
(lambda ()
(interactive)
(ignore-errors (forward-line 5))))
(global-set-key (kbd "C-S-p")
(lambda ()
(interactive)
(ignore-errors (forward-line -5))))
(global-set-key (kbd "C-S-f")
(lambda ()
(interactive)
(ignore-errors (forward-char 5))))
(global-set-key (kbd "C-S-b")
(lambda ()
(interactive)
(ignore-errors (backward-char 5))))Some packages bind Super key combinations (e.g. s-g, s-r), which may cause startup errors on systems where Super is not a recognized modifier. This block disables those bindings safely.
;; Remove problematic Super keybindings after init
(add-hook 'after-init-hook
(lambda ()
;; Unbind from global-map safely
(define-key global-map (kbd "s-g") nil)
(define-key global-map (kbd "s-r") nil)
(define-key global-map (kbd "s-s") nil)
(define-key global-map (kbd "s-x") nil)))
;; Also remove Super bindings from projectile-mode-map after it's loaded
(with-eval-after-load 'projectile
(define-key projectile-mode-map (kbd "s-g") nil)
(define-key projectile-mode-map (kbd "s-r") nil)
(define-key projectile-mode-map (kbd "s-s") nil)
(define-key projectile-mode-map (kbd "s-x") nil))(defun ygg/unbind-super-globally ()
"Unbind all known s-* keys that might be bound by packages or OS overrides."
(dolist (key '("s-g" "s-r" "s-s" "s-x" "s-i"))
(when (stringp key)
(ignore-errors (define-key global-map (kbd key) nil)))))
(add-hook 'after-init-hook #'ygg/unbind-super-globally)
(with-eval-after-load 'projectile
(dolist (key '("s-g" "s-r" "s-s" "s-x" "s-i"))
(ignore-errors (define-key projectile-mode-map (kbd key) nil))))Override the package shortcuts that use the Super Key
(define-prefix-command 'my/project-map)
(global-set-key (kbd "C-c C-p") 'my/project-map)
(with-eval-after-load 'projectile
(define-key my/project-map (kbd "f") #'projectile-find-file)
(define-key my/project-map (kbd "s") #'projectile-ripgrep)
(define-key my/project-map (kbd "p") #'projectile-switch-project))Hydra provides a transient menu for fast access to project tools.
Use C-c C-h to bring it up.
(use-package hydra
:ensure t)
(defhydra hydra-project (:color blue :hint nil)
"
๐ Project Tools:
_f_: Find file _s_: Search (ripgrep) _g_: Grep
_x_: References _p_: Switch project _q_: Quit
"
("f" counsel-projectile-find-file)
("s" counsel-projectile-rg)
("g" projectile-grep)
("x" projectile-find-references)
("p" counsel-projectile-switch-project)
("q" nil "Quit"))
(global-set-key (kbd "C-c C-h") 'hydra-project/body)