emacs.d

My Emacs configuration
git clone https://git.jamzattack.xyz/emacs.d
Log | Files | Refs | LICENSE

ModeNameSize
-rw-r--r--.gitignore44L
-rw-r--r--LICENSE675L
l---------README.org1L
-rw-r--r--config.org4301L
-rw-r--r--eshell/alias8L
-rw-r--r--init.el40L
-rw-r--r--lisp/abbrev/text-mode-abbrevs.el203L
-rw-r--r--lisp/arch-linux-settings/arch-linux-settings.el6L
-rw-r--r--lisp/bitmaps/my-bitmaps.el68L
-rw-r--r--lisp/ed/ed.el50L
-rw-r--r--lisp/exwm/custom-exwm-config.el237L
-rw-r--r--lisp/fonts.el78L
-rw-r--r--lisp/helm/custom-helm-bookmark.el179L
-rw-r--r--lisp/library-genesis/library-genesis.el88L
-rw-r--r--lisp/minibuffer-hacks/minibuffer-hacks.el46L
-rw-r--r--lisp/my-misc-defuns/my-misc-defuns.el218L
-rw-r--r--lisp/org/ox-groff.el1960L
-rw-r--r--lisp/reddit-browse/reddit-browse.el49L
-rw-r--r--lisp/themes/custom-dark-theme.el41L
-rw-r--r--lisp/themes/custom-theme.el67L
-rw-r--r--lisp/toggle-touchpad/toggle-touchpad.el128L
Emacs Configuration

Emacs Configuration

This is my Emacs config file. It is written in org-mode so that I can brag about how dope org-mode is. This file contains my main configuration, which is tangled to config.el. init.el sets the variable custom-file to custom.el, loads config.el, and then loads custom.el.

Note: custom.el should not be edited manually, as it is used by Emacs for settings changed using the customisation interface.

My own packages and other things that I need to require are housed in lisp/. I don't include the straight/ directory so the first startup will take some time.

Startup

Get some things out of the way early. Without straight or use-package, none of this config would work.

Straight

Install straight.el. It gets a lot of hype, so I'm trying to use it instead of the built-in package.el. It has a use-package keyword, so you can simply (re)define a package like so:

(use-package package-name
  :straight
  (package-name :host gitlab
                :repo "user/forked-package"
                :branch "cool-new-feature"))

This snippet clones and loads straight, stolen from the README.

(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 5))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(with-eval-after-load 'straight
  (setq straight-vc-git-default-protocol 'ssh)
  (add-to-list 'straight-built-in-pseudo-packages 'org)
  (fset 'try #'straight-use-package)
  (defun straight-clone-package (pkg)
    "Clone PKG via straight without building or loading it.
This function is for interactive use only.  From lisp,
use `(straight-use-package PKG nil t)' instead."
    (declare (interactive-only
              "Use `straight-use-package' with the NO-BUILD argument instead"))
    (interactive (list (straight-get-recipe (when current-prefix-arg 'interactive))))
    (straight-use-package pkg nil t)))

Use-package and dependencies

Install use-package using straight-use-package, and load both use-package and bind-key. Note bind-key is a dependency of use-package, so I don't need to install it manually.

I also use use-package's :delight keyword, so install that as well. I don't need to use (require delight) as use-package handles that.

(straight-use-package 'use-package)
(straight-use-package 'delight)
(setq use-package-compute-statistics t)
(require 'use-package)
(require 'bind-key)

Fonts

The function set-up-fonts-please loads my font settings. Call it when creating a new frame or starting emacs. The weird hack bit makes sure that exwm's additions are run before settings the fonts.

(defun set-up-fonts-please (&optional frame)
  "Load font settings in `user-emacs-directory'/lisp/fonts.el."
  (interactive)
  (with-selected-frame (or frame (selected-frame))
    (load (expand-file-name "lisp/fonts.el" user-emacs-directory))))

(add-hook 'server-after-make-frame-hook 'set-up-fonts-please)
(add-hook 'window-setup-hook 'set-up-fonts-please)
(add-to-list 'after-make-frame-functions 'set-up-fonts-please)
(with-eval-after-load 'custom-exwm-config
  (setq after-make-frame-functions
        (cons 'set-up-fonts-please
              (remove 'set-up-fonts-please after-make-frame-functions))))

Keybindings

Prefix keys

A couple of prefix keys. It's useful to set these up early, so that you don't get any errors i.e "C-z is not a valid prefix key".

  • Remove C-z

    Unbind C-z before anything else, so that I can use it as a prefix key.

    (global-unset-key (kbd "C-z"))
    
  • Alias <menu> to C-x

    Make the menu key do the same as C-x.

    (bind-key "<menu>" ctl-x-map)
    

Reloading config file

Reload config file with C-z C-r. This is done with bind-key so that it is recorded in the variable personal-keybindings.

(bind-key "C-z C-r" 'config-load)

Built-in packages

This is the section for built-in packages.

package.el

It's useful to keep package.el updated for the functions describe-package and list-packages. All my packages are now installed using straight, so disable the function package-install.

(use-package package
  :no-require t
  :config
  (setq package-archives
        '(("gnu" . "http://elpa.gnu.org/packages/")
          ("melpa" . "http://melpa.org/packages/")))
  (fmakunbound 'package-install))

Major editing modes

Major modes for text editing. For non-editing major modes, see Applications

Org Mode

Open source blocks and stuff in the current window. Use TAB from the language's major mode inside source blocks. Open everything in Emacs, and use eww for html instead of mhtml-mode.

(use-package org
  :defer t
  :custom
  (org-src-window-setup 'current-window)
  (org-src-tab-acts-natively t)
  (org-adapt-indentation nil)
  (org-hide-emphasis-markers t)
  (org-file-apps
   '((auto-mode . emacs)
     ("\\.x?html?\\'" . (lambda (file &optional ignore)
                          (eww-open-file file)))))
  :delight
  (org-src-mode " #+src")
  :config
  ;; Quite ugly: (setf (last ...)) doesn't exist, and can't use
  ;; assoc/alist-get because the package name is the cadr
  (setf (nth (1- (length org-latex-default-packages-alist))
             org-latex-default-packages-alist)
        '("hidelinks" "hyperref" nil)
        (car org-latex-default-packages-alist)
        '("utf8x" "inputenc" "pdflatex"))
  (defun org-insert-emacs-help (&optional prompt)
    "Insert a help link to a symbol.
  If the symbol at point is bound, it is replaced by the link.
  Otherwise, or with prefix arg, PROMPT from all bound symbols in
  `obarray'."
    (interactive "*P")
    (when (eq (get-text-property (point) 'face)
              'org-link)
      (user-error "Text at point is already a link--don't want to mangle the buffer"))
    (cl-labels ((predicate (sym)
                           (and (or (boundp sym)
                                    (fboundp sym))
                                (not (keywordp sym))))
                (prompt ()
                        (completing-read
                         "Help link: "
                         obarray
                         #'predicate
                         t)))
      (let ((symbol
             (or (when prompt
                   (prompt))
                 (let ((symbol (symbol-at-point))
                       (bounds (bounds-of-thing-at-point 'symbol)))
                   (when (and symbol
                              (predicate symbol))
                     (delete-region (car bounds) (cdr bounds))
                     symbol))
                 (prompt))))
        (insert (format "[[help:%s][%s]]" symbol symbol)))))
  :bind
  ("C-c M-." . org-time-stamp)
  (:map org-mode-map
        ("C-c C-v h" . org-hide-block-all)
        ("M-h" . mark-paragraph)
        ("C-M-h" . org-mark-element)
        ("C-c h" . org-insert-emacs-help)))
  • Insert help link

    A function to insert an org-mode help link. This uses the symbol at point if it's a defined variable or function. Otherwise, it prompts from all bound or fbound symbols.

    (defun org-insert-emacs-help (&optional prompt)
      "Insert a help link to a symbol.
    If the symbol at point is bound, it is replaced by the link.
    Otherwise, or with prefix arg, PROMPT from all bound symbols in
    `obarray'."
      (interactive "*P")
      (when (eq (get-text-property (point) 'face)
                'org-link)
        (user-error "Text at point is already a link--don't want to mangle the buffer"))
      (cl-labels ((predicate (sym)
                             (and (or (boundp sym)
                                      (fboundp sym))
                                  (not (keywordp sym))))
                  (prompt ()
                          (completing-read
                           "Help link: "
                           obarray
                           #'predicate
                           t)))
        (let ((symbol
               (or (when prompt
                     (prompt))
                   (let ((symbol (symbol-at-point))
                         (bounds (bounds-of-thing-at-point 'symbol)))
                     (when (and symbol
                                (predicate symbol))
                       (delete-region (car bounds) (cdr bounds))
                       symbol))
                   (prompt))))
          (insert (format "[[help:%s][%s]]" symbol symbol)))))
    
  • Org Indent

    I used to use org-indent-mode a while back, but ditched it for reasons I can't remember. I set the indentation level to 1 character instead of its default value of 2. This helps to keep the text within a manageable width and is probably the reason I disabled it.

    I find org-mode looks a bit cleaner and more "open" with this mode enabled. Without it, the window can get cluttered pretty easily.

    (use-package org-indent
      :defer
      :delight
      :config
      (setq org-indent-indentation-per-level 1))
    
  • Org capture

    Take notes in org-mode with specific templates and write them to a file. Similar to remember.

    (use-package org-capture
      :custom
      (org-default-notes-file "~/org/notes.org")
      (org-capture-templates
       '(("t" "Todo")
         ("tt" "Misc." entry
          (file+headline "todo.org" "Miscellaneous")
          "* TODO %?\n\n%a\n")
         ("tu" "University" entry
          (file+headline "todo.org" "University")
          "* TODO %?\n\n%a\n")
         ("n" "Notes" entry
          (file+headline "notes.org" "Notes")
          "* %?\nEntered on %u\n\n%i\n\n%a\n")
         ("m" "Music" entry
          (file+headline "notes.org" "Music")
          "* %?\nEntered on %u\n\n%i\n")
         ("e" "Elisp" entry
          (file+headline "notes.org" "Emacs Lisp")
          "* %^{Title}\n\n#+begin_src emacs-lisp\n %i\n#+end_src\n")
         ("d" "Diary" entry
          (file "diary.org")
          "* %?\nEntered on %u\n\n")
         ("h" "Haiku" entry
          (file "haiku.org")
          "* %U\n%?\n")))
      (org-capture-bookmark nil)
      :bind
      ("C-x M-r" . org-capture))
    
  • Org babel

    Work with code blocks. The libraries all provide support for a language so that you can run their source blocks with C-c C-c.

    • LilyPond

      Execute LilyPond source blocks. For notes about exporting to pdf, see this org file. Only load it when lilypond is installed.

      (use-package ob-lilypond
        :when (executable-find "lilypond")
        :defer t
        :config
        (defun ob-lilypond-pdf-or-png (backend &rest _args)
          "Replace the lilypond source blocks' :file argument.
        This will turn them all into .png files if BACKEND is html, and
        .pdf files in BACKEND is latex."
          (when (member backend '(latex html))
            (let ((case-fold-search t))
              (save-excursion
                (goto-char (point-min))
                (while (re-search-forward
                        "^\\(#\\+begin_src lilypond .*:file \\)\\(.*\\)\\.[a-z]+"
                        nil :noerror)
                  (replace-match (pcase backend
                                   ('latex "\\1\\2.pdf")
                                   ('html "\\1\\2.png")))))
              (save-buffer))))
      
        (advice-add 'org-export-to-file :before #'ob-lilypond-pdf-or-png)
        :commands org-babel-execute:lilypond)
      
      • Replace :file argument in lilypond source blocks

        This little bit of hackery to adjust the :file argument for lilypond source blocks.

        • pdf works great with latex export, but doesn't work with html.
        • png works great with html export, but looks fuzzy with latex.

        This advice checks the backend of the export to determine which to use.

        (defun ob-lilypond-pdf-or-png (backend &rest _args)
          "Replace the lilypond source blocks' :file argument.
        This will turn them all into .png files if BACKEND is html, and
        .pdf files in BACKEND is latex."
          (when (member backend '(latex html))
            (let ((case-fold-search t))
              (save-excursion
                (goto-char (point-min))
                (while (re-search-forward
                        "^\\(#\\+begin_src lilypond .*:file \\)\\(.*\\)\\.[a-z]+"
                        nil :noerror)
                  (replace-match (pcase backend
                                   ('latex "\\1\\2.pdf")
                                   ('html "\\1\\2.png")))))
              (save-buffer))))
        
        (advice-add 'org-export-to-file :before #'ob-lilypond-pdf-or-png)
        
    • C

      Execute C source blocks. TCC is a really fast compiler, so use it instead of gcc if it's installed.

      (use-package ob-C
        :defer t
        :commands org-babel-execute:C
        :custom
        (org-babel-C-compiler
         (or (executable-find "tcc")
             "gcc")))
      
    • Scheme

      Execute scheme source blocks. This uses Geiser which is kind of awkward and slow, but evaluating scheme is useful.

      (use-package ob-scheme
        :defer t
        :commands org-babel-execute:scheme)
      
    • Common Lisp

      Execute Common Lisp source blocks. This depends on Slime, which doesn't start automatically (see the variable slime-auto-start).

      (use-package ob-lisp
        :defer t
        :commands org-babel-execute:lisp)
      
    • Shell

      Execute shell source blocks. Autoload sh, shell, and bash functions.

      (use-package ob-shell
        :defer t
        :commands
        org-babel-execute:sh
        org-babel-execute:shell
        org-babel-execute:bash)
      
  • Org links

    The library org-mode uses to create and store links. I bind C-x M-l to generate a link from the current position.

    (use-package ol
      :config
      (defun ol-help--export (link description format)
        (let* ((desc (or description link))
               (sym (intern link))
               (type (if (fboundp sym)
                         "Fun"
                       "Var")))
          (when (eq format 'html)
            (format "<a target=\"_blank\" href=\"https://doc.endlessparentheses.com/%s/%s.html\">%s</a>"
                    type link desc))))
    
      (org-link-set-parameters "help" :export #'ol-help--export)
      :bind
      ("C-x M-l" . org-store-link))
    
    • Export help links in html

      I often use org's help links (e.g. org-mode) but by default these are useless in an html export. Thankfully, there's a neat site that contains docstrings for all the built-in definitions. Here, I:

      1. define a function that formats an href to the symbol's respective page, and
      2. let org know that I want to use that function whenever a help link is exported
      (defun ol-help--export (link description format)
        (let* ((desc (or description link))
               (sym (intern link))
               (type (if (fboundp sym)
                         "Fun"
                       "Var")))
          (when (eq format 'html)
            (format "<a target=\"_blank\" href=\"https://doc.endlessparentheses.com/%s/%s.html\">%s</a>"
                    type link desc))))
      
      (org-link-set-parameters "help" :export #'ol-help--export)
      
  • Org agenda

    Use all files in org-directory to get my agenda. And don't disrupt my window configuration.

    (use-package org-agenda
      :defer t
      :custom
      (org-agenda-files '("~/org" "~/org/uni"))
      (org-agenda-window-setup 'current-window)
      :bind
      ("C-z C-a" . org-agenda))
    
  • Org publish

    I use org-publish for my websites. This block has a lot going on:

    1. I set some default options for publishing projects.
    2. I use a custom function to generate postamble.
    3. Include my three sites in org-publish-project-alist.
    (use-package ox-publish
      :defer t
      :config
      (use-package ox-jamzattack
        :demand
        :straight
        (ox-jamzattack :type git
                       :repo "git@jamzattack.xyz:ox-jamzattack.git"))
      (defun my-org-html-postamble-format (&rest args)
        "Generate an html postamble using ARGS.
      This generates a paragraph for each item in ARGS.  For format
      strings, see the docstring of `org-html-postamble-format'."
        (unless args
          (setq args '("Author: %a <%e>")))
        (list (list "en"
                    (mapconcat (lambda (str)
                                 (format (cond
                                          ((string-match-p "%d" str)
                                           "<p class=\"date\">%s</p>")
                                          ((string-match-p "%A" str)
                                           "<p class=\"author\">%s</p>")
                                          (t
                                           "<p>%s</p>"))
                                         str))
                               args
                               "\n"))))
      (defvar my-org-publish-default-options
        '(
          :auto-sitemap t
          :publishing-function org-html-publish-to-html
          :html-metadata-timestamp-format "%Y-%m-%d"
          :with-toc nil
          :with-email t
          :with-drawers nil
          :section-numbers nil
          :with-todo-keywords nil
          )
        "Default options for `org-publish-project-alist'.
    This variable must be spliced into `org-publish-project-alist'
    when set, i.e.
        (setq org-publish-project-alist
                `((\"project\"
                   ,@my-org-publish-default-options)))")
      (setq
       org-html-postamble t ; needed to use custom format
       org-export-headline-levels 6
       org-html-postamble-format
       (my-org-html-postamble-format
        "Author: %A")
       org-publish-timestamp-directory "~/.cache/org/timestamps/"
       org-html-head "<link rel=\"stylesheet\" type=\"text/css\" href=\"/style.css\"/>"
       org-publish-project-alist
       `(("blog"
          ,@my-org-publish-default-options
          :base-directory "~/org/jamzattack.xyz/blog"
          :with-toc t
          :publishing-directory "~/org/jamzattack.xyz/out/blog"
          :html-postamble-format ,(my-org-html-postamble-format
                                   "Author: %A"
                                   "Date: %d (modified %M)"
                                   "Top: <a href=\"/index.html\">The Yeet Log</a>")
          :sitemap-filename "index.org"
          :sitemap-title "The Yeet Log"
          :sitemap-format-entry
          (lambda (entry style project)
            (cond ((not (directory-name-p entry))
                   (format "%s [[file:%s][%s]]"
                           (format-time-string
                            "%Y-%m-%d"
                            (org-publish-find-date entry project))
                           entry
                           (org-publish-find-title entry project)))
                  ((eq style 'tree)
                   ;; Return only last subdir.
                   (file-name-nondirectory (directory-file-name entry)))
                  (t entry)))
          :sitemap-sort-files anti-chronologically)
         ("gopher"
          :base-directory "~/org/jamzattack.xyz/blog"
          :with-toc t
          :with-email t
          :section-numbers nil
          :publishing-function org-ascii-publish-to-ascii
          :publishing-directory "~/org/jamzattack.xyz/out/gopher")
         ("music"
          ,@my-org-publish-default-options
          :base-directory "~/org/jamzattack.xyz/music"
          :recursive t
          :html-postamble-format ,(my-org-html-postamble-format
                                   "Author: %A"
                                   "Top: <a href=\"/sitemap.html\">All projects</a>")
          :publishing-directory "~/org/jamzattack.xyz/out/music"
          :sitemap-title "My Music Projects")
         ("html"
          ,@my-org-publish-default-options
          :base-directory "~/org/jamzattack.xyz/html"
          :publishing-directory "~/org/jamzattack.xyz/out/html"))))
    
    • Generate postamble

      A little function to generate postamble.

      (defun my-org-html-postamble-format (&rest args)
        "Generate an html postamble using ARGS.
      This generates a paragraph for each item in ARGS.  For format
      strings, see the docstring of `org-html-postamble-format'."
        (unless args
          (setq args '("Author: %a <%e>")))
        (list (list "en"
                    (mapconcat (lambda (str)
                                 (format (cond
                                          ((string-match-p "%d" str)
                                           "<p class=\"date\">%s</p>")
                                          ((string-match-p "%A" str)
                                           "<p class=\"author\">%s</p>")
                                          (t
                                           "<p>%s</p>"))
                                         str))
                               args
                               "\n"))))
      
    • Default export options

      A list of default export options.

      :auto-sitemap t
      :publishing-function org-html-publish-to-html
      :html-metadata-timestamp-format "%Y-%m-%d"
      :with-toc nil
      :with-email t
      :with-drawers nil
      :section-numbers nil
      :with-todo-keywords nil
      

Cc-mode

Set the C style to bsd, which uses tabs. Use Java/Awk indentation for Java/Awk files.

(use-package cc-mode
  :defer t
  :custom
  (c-default-style '((java-mode . "java")
                     (awk-mode . "awk")
                     (other . "bsd"))))

Emacs Lisp mode

Make the scratch buffer use emacs-lisp-mode. Note: Most of my Elisp keybindings are now in my package selime.

(use-package elisp-mode
  :custom
  (initial-major-mode 'emacs-lisp-mode)
  :delight
  (emacs-lisp-mode ("el" (lexical-binding "/l" "/d")) :major)
  (inferior-emacs-lisp-mode "EL>" :major)
  :bind
  ("<C-M-backspace>" . backward-kill-sexp))
  • Find-func

    A package that defines a few functions for editing Elisp source code. It provides the function find-function-setup-keys which binds some keys in ctl-x-map, but I prefer to have them under C-h.

    (use-package find-func
      :defer t
      :bind
      (:map help-map
            ("C-l" . find-library)
            ("C-f" . find-function)
            ("C-v" . find-variable)
            ("C-k" . find-function-on-key)))
    

Typesetting

  • Nroff-mode

    Set a compile-command hook for nroff files. I usually use the ms macros when writing something, but I usually just use org-mode anyway.

    (use-package nroff-mode
      :defer t
      :config
      (defun nroff-mode-compile ()
        "Set the compile command for nroff files.
      It will choose the macro set based on the file extension."
        (let* ((in (buffer-file-name))
               (out (concat (file-name-sans-extension in)
                            ".pdf")))
          (setq-local
           compile-command
           (format "groff -%s -Tpdf '%s' > '%s'"
                   (file-name-extension in) in out))))
      :hook (nroff-mode . nroff-mode-compile))
    
    • Compile Command
      (defun nroff-mode-compile ()
        "Set the compile command for nroff files.
      It will choose the macro set based on the file extension."
        (let* ((in (buffer-file-name))
               (out (concat (file-name-sans-extension in)
                            ".pdf")))
          (setq-local
           compile-command
           (format "groff -%s -Tpdf '%s' > '%s'"
                   (file-name-extension in) in out))))
      
  • LaTeX

    Set a compile-command hook for latex files. I prefer to write in org-mode, but compiling latex on its own is sometimes useful.

    (use-package tex-mode
      :defer t
      :config
      (defun latex-compile-command ()
        "Set the compile command for latex files."
        (setq-local compile-command
                    (format "pdflatex %s" buffer-file-name)))
      :hook (latex-mode . latex-compile-command))
    
    • Compile Command
      (defun latex-compile-command ()
        "Set the compile command for latex files."
        (setq-local compile-command
                    (format "pdflatex %s" buffer-file-name)))
      

Minor modes

Minor modes that help with anything Emacs, be it programming, writing emails, or anything else that Emacs can do.

Compile

Bind C-z RET to compile and f9 to recompile (like compile, but no need to press RET).

Also provided by this library is compilation-shell-minor-mode, a minor mode designed for Shell that provides highlighting and navigation for errors and warnings. I enable it in both Shell and Eshell.

(use-package compile
  :bind
  ("C-z C-m" . compile)
  ("<f9>" . recompile)
  :delight
  (compilation-shell-minor-mode " ¢")   ; "C" for compile...
  :hook
  (eshell-mode . compilation-shell-minor-mode)
  (shell-mode . compilation-shell-minor-mode))

Hi-lock

global-hi-lock-mode binds a bunch of useful keys, but here I bind them manually to allow autoloading. I also bind C-c . to my most used command, highlight-symbol-at-point.

(use-package hi-lock
  :delight
  :bind
  ("C-c ." . highlight-symbol-at-point)
  ("C-x w i" . hi-lock-find-patterns)
  ("C-x w l" . highlight-lines-matching-regexp)
  ("C-x w p" . highlight-phrase)
  ("C-x w h" . highlight-regexp)
  ("C-x w ." . highlight-symbol-at-point)
  ("C-x w r" . unhighlight-regexp)
  ("C-x w b" . hi-lock-write-interactive-patterns))

Parens

Highlight matching parens everywhere.

(use-package paren
  :config
  (show-paren-mode t))

Auto fill

Instead of "Fill", show ^M (carriage return) in the mode-line.

(use-package simple
  :delight
  (auto-fill-function " ^M"))

Isearch

Instead of "ISearch", show ^S (C-s) in the mode-line.

(use-package isearch
  :delight " ^S")

Eldoc

Eldoc is what provides the function signature in the mode-line when editing Elisp. By default, it waits for 0.5 seconds so I bump the delay down to 0.1.

(use-package eldoc
  :delight
  :defer t
  :custom
  (eldoc-idle-delay 0.1)
  :config
  (eldoc-add-command
   ;; Moving
   'paredit-backward
   'paredit-forward
   'paredit-forward-down
   'paredit-backward-up
   'paredit-backward-down
   'paredit-forward-up
   ;; Editing
   'paredit-raise-sexp
   'paredit-splice-sexp-killing-backward
   'paredit-convolute-sexp
   'paredit-close-round
   'paredit-close-round-and-newline
   'paredit-forward-delete
   'paredit-backward-delete))

Applications

This section is for Elisp programs that have an interface of their own, rather than being just a major/minor mode.

EWW

Elisp web browser - I just set some variables to make eww the default browser, and change the width to 80 columns.

(use-package eww
  :defer t
  :custom
  (eww-bookmarks-directory
   (expand-file-name "eww" user-emacs-directory))
  (eww-browse-url-new-window-is-tab nil)
  :init
  (with-eval-after-load 'browse-url
    (setq browse-url-browser-function 'eww-browse-url
          browse-url-secondary-browser-function 'browse-url-externally-please))
  (defun browse-url-externally-please (url &optional ignored)
    "Open URL using either vimb or surf if they are found,
  otherwise use xdg-open."
    (interactive (browse-url-interactive-arg "URL: "))
    (call-process (or (executable-find "vimb")
                      (executable-find "surf")
                      (executable-find "xdg-open"))
                  nil 0 nil url))
  :config
  (defun eww-edit-current-url (&optional arg)
    "Edit the current URL.
  With prefix ARG, open in a new buffer."
    (interactive "p")
    (let ((url
           (read-string (if (= arg 1)
                            "URL: "
                          "URL (new buffer): ")
                        (eww-current-url))))
      (eww url arg)))
  (defun eww-set-width (width)
    "Set the html rendering width to WIDTH.
  If prefix arg is a number, use it.  Otherwise, read number from
  the minibuffer."
    (interactive (list
                  (if (numberp current-prefix-arg)
                      current-prefix-arg
                    (read-number "Set width: "
                                 (- (window-width) 5)))))
    (setq-local shr-width width)
    (eww-reload t))
  (defun eww-follow-link-with-browse-url (&optional external mouse-event)
    "Browse the URL under point.
  If EXTERNAL is single prefix, browse the URL using
  `browse-url-secondary-browser-function'."
    (interactive (list current-prefix-arg last-nonmenu-event))
    (mouse-set-point mouse-event)
    (let ((url (get-text-property (point) 'shr-url)))
      (cond
       ((not url)
        (message "No link under point"))
       ((and (consp external) (<= (car external) 4))
        (funcall browse-url-secondary-browser-function url)
        (shr--blink-link))
       ;; This is a #target url in the same page as the current one.
       ((and (url-target (url-generic-parse-url url))
             (eww-same-page-p url (plist-get eww-data :url)))
        (let ((dom (plist-get eww-data :dom)))
          (eww-save-history)
          (plist-put eww-data :url url)
          (eww-display-html 'utf-8 url dom nil (current-buffer))))
       (t
        (let ((browse-url-browser-function #'eww-browse-url))
          (browse-url url external))))))

  (advice-add 'eww-follow-link :override #'eww-follow-link-with-browse-url)
  :bind
  ("C-z g" . eww)
  (:map eww-mode-map
        ("M-n" . forward-paragraph)
        ("M-p" . backward-paragraph)
        ("e" . eww-edit-current-url)
        ("V" . variable-pitch-mode)
        ("C-x f" . eww-set-width)
        ;; plumb
        ("f" . plumb-stream)
        ("D" . plumb-download-video)
        ("A" . plumb-audio)
        ;; transmission
        ("m" . transmission-add-url-at-point)
        ;; helm-eww
        ("B" . helm-eww-bookmarks)
        ("H" . helm-eww-history)
        ("s" . helm-eww-buffers)))
  • External browser
    (defun browse-url-externally-please (url &optional ignored)
      "Open URL using either vimb or surf if they are found,
    otherwise use xdg-open."
      (interactive (browse-url-interactive-arg "URL: "))
      (call-process (or (executable-find "vimb")
                        (executable-find "surf")
                        (executable-find "xdg-open"))
                    nil 0 nil url))
    
  • Edit current URL

    Useful command to edit the current URL. With prefix arg, open the edited URL in a new buffer. Bound to e in eww-mode.

    (defun eww-edit-current-url (&optional arg)
      "Edit the current URL.
    With prefix ARG, open in a new buffer."
      (interactive "p")
      (let ((url
             (read-string (if (= arg 1)
                              "URL: "
                            "URL (new buffer): ")
                          (eww-current-url))))
        (eww url arg)))
    
  • Set eww width

    This command sets shr-width to a value read from the minibuffer. Very useful in eww, and a fitting replacement for set-fill-column.

    (defun eww-set-width (width)
      "Set the html rendering width to WIDTH.
    If prefix arg is a number, use it.  Otherwise, read number from
    the minibuffer."
      (interactive (list
                    (if (numberp current-prefix-arg)
                        current-prefix-arg
                      (read-number "Set width: "
                                   (- (window-width) 5)))))
      (setq-local shr-width width)
      (eww-reload t))
    
  • Follow links using browse-url

    The key RET in eww is bound to eww-follow-link, which bypasses browse-url-handlers meaning you can't open non-http links (except for the one exception, mailto). Here I override this function to use browse-url, and ensure that eww is used where possible. In effect, this means I can open gopher links from eww in elpher.

    (defun eww-follow-link-with-browse-url (&optional external mouse-event)
      "Browse the URL under point.
    If EXTERNAL is single prefix, browse the URL using
    `browse-url-secondary-browser-function'."
      (interactive (list current-prefix-arg last-nonmenu-event))
      (mouse-set-point mouse-event)
      (let ((url (get-text-property (point) 'shr-url)))
        (cond
         ((not url)
          (message "No link under point"))
         ((and (consp external) (<= (car external) 4))
          (funcall browse-url-secondary-browser-function url)
          (shr--blink-link))
         ;; This is a #target url in the same page as the current one.
         ((and (url-target (url-generic-parse-url url))
               (eww-same-page-p url (plist-get eww-data :url)))
          (let ((dom (plist-get eww-data :dom)))
            (eww-save-history)
            (plist-put eww-data :url url)
            (eww-display-html 'utf-8 url dom nil (current-buffer))))
         (t
          (let ((browse-url-browser-function #'eww-browse-url))
            (browse-url url external))))))
    
    (advice-add 'eww-follow-link :override #'eww-follow-link-with-browse-url)
    

SHR

(use-package shr
  :defer t
  :custom
  (shr-width 80)
  :config
  (defun un-duckduckgo-url (args)
    "Cleanse a url from duckduckgo's janky redirect.
  This takes the same args as `shr-urlify', passed as a list."
    (let ((start (nth 0 args))
          (url (nth 1 args))
          (title (nth 2 args)))
      (list start
            (let ((unhexed (url-unhex-string url))
                  (regexp "\\`.*[&\\?]uddg=\\(.*\\)&rut=[a-z0-9]\\{64\\}"))
              (if (string-match regexp unhexed)
                  (match-string 1 unhexed)
                url))
            title)))

  (advice-add 'shr-urlify :filter-args #'un-duckduckgo-url)
  :bind
  (:map shr-map
        ("f" . plumb-stream)
        ("A" . plumb-audio)
        ("D" . plumb-download-video)))
  • Remove duckduckgo tracking from url

    Duckduckgo does a very sinful thing – instead of linking to https://url.com, it links to:

    https://duckduckgo.com/l/?kh=-1&uddg=https%3A%2F%2Furl.com
    
    

    Here, I define a function that removes all this junk, and use advice to filter the arguments given to shr-urlify. Because this is relatively low-level, all occurences of duckduckgo's redirects that are parsed with shr are replaced with the clean version.

    (defun un-duckduckgo-url (args)
      "Cleanse a url from duckduckgo's janky redirect.
    This takes the same args as `shr-urlify', passed as a list."
      (let ((start (nth 0 args))
            (url (nth 1 args))
            (title (nth 2 args)))
        (list start
              (let ((unhexed (url-unhex-string url))
                    (regexp "\\`.*[&\\?]uddg=\\(.*\\)&rut=[a-z0-9]\\{64\\}"))
                (if (string-match regexp unhexed)
                    (match-string 1 unhexed)
                  url))
              title)))
    
    (advice-add 'shr-urlify :filter-args #'un-duckduckgo-url)
    

ERC

ERC is perhaps the greatest IRC client ever made. I use ZNC on my server, so I connect to that, and set my password in my authinfo file.

(use-package erc
  :defer t
  :custom
  (erc-server "jamzattack.xyz")
  (erc-nick "jamzattack")
  (erc-hide-list '("JOIN" "PART" "QUIT"))
  :config
  (defun znc-detach-channel ()
    "Hook that handles ZNC-specific channel killing behavior"
    (when (erc-server-process-alive)
      (when-let ((tgt (erc-default-target)))
        (erc-server-send (format "DETACH %s" tgt)
                         nil tgt))))

  (advice-add 'erc-kill-channel :override #'znc-detach-channel)
  (defun erc-narrow-to-znc-playback (&optional force)
    "Narrow the buffer beginning at the latest buffer playback.
  If the buffer is already narrowed beyond that point, don't change
  anything.  With prefix arg FORCE, extend the buffer to the
  playback even if it is already narrowed."
    (interactive "P")
    (save-excursion
      (when force
        (widen))
      (narrow-to-region
       (goto-char (point-max))
       (or (re-search-backward
            (rx bol "<***> Buffer Playback..." eol)
            nil :noerror)
           (point-min)))))

  (define-key erc-mode-map (kbd "C-c b") #'erc-narrow-to-znc-playback)
  (add-to-list 'erc-modules 'notifications)
  (erc-update-modules)
  (erc-track-mode))
  • Detach instead of parting when buffer is killed

    I've just started using ZNC, an IRC bouncer. ERC, however tries to part from a channel when its buffer is killed. Instead, I want to detach so that I can reattach later. Here, I override erc-kill-channel, resulting in the wanted behaviour.

    (defun znc-detach-channel ()
      "Hook that handles ZNC-specific channel killing behavior"
      (when (erc-server-process-alive)
        (when-let ((tgt (erc-default-target)))
          (erc-server-send (format "DETACH %s" tgt)
                           nil tgt))))
    
    (advice-add 'erc-kill-channel :override #'znc-detach-channel)
    
  • Narrow to ZNC playback

    When reattaching to a channel via ZNC, it plays back some number of recent messages and sends them upon connection. This function narrows to the most recent playback, unless the buffer is already narrowed further.

    I would have liked to use a hook to do this automatically, but due to the asynchronous mechanics of the system I'm gonna have to make do with a keybind.

    (defun erc-narrow-to-znc-playback (&optional force)
      "Narrow the buffer beginning at the latest buffer playback.
    If the buffer is already narrowed beyond that point, don't change
    anything.  With prefix arg FORCE, extend the buffer to the
    playback even if it is already narrowed."
      (interactive "P")
      (save-excursion
        (when force
          (widen))
        (narrow-to-region
         (goto-char (point-max))
         (or (re-search-backward
              (rx bol "<***> Buffer Playback..." eol)
              nil :noerror)
             (point-min)))))
    
    (define-key erc-mode-map (kbd "C-c b") #'erc-narrow-to-znc-playback)
    
  • ERC notifications

    erc-notify enables notifications for erc conversations. I only enable it if the executable "dunst" is found, because it will crash Emacs unless a notification daemon is active.

    (use-package erc-notify
      :after erc
      :config
      (when (executable-find "dunst")
        (erc-notify-enable)))
    

Info

Rebind M-p and M-n to move by paragraphs. By default M-n runs clone-buffer, which I find to be completely useless.

(use-package info
  :bind
  (:map Info-mode-map
        ("M-p" . backward-paragraph)
        ("M-n" . forward-paragraph)))

Ibuffer

Ibuffer is an interface similar to dired, but for editing your open buffers. I don't use it much now in favour of Helm, but it can be useful for more complex filtering.

(use-package ibuffer
  :bind
  ("C-x C-b" . ibuffer)
  :init
  (defun ibuffer-helm-major-mode-predicate (buffer)
    "Returns t if BUF is a helm buffer."
    (equal 'helm-major-mode (buffer-local-value 'major-mode buffer)))
  :config
  (add-to-list 'ibuffer-maybe-show-predicates
               #'ibuffer-helm-major-mode-predicate))

Dired

Group directories first. This works only with GNU ls, so don't use this if you use a different version.

(use-package dired
  :defer t
  :config
  (setq dired-listing-switches "-lahv --group-directories-first")
  :init
  (setq delete-by-moving-to-trash t))

Diffing

  • Ediff

    By default, Ediff tries to open its own frame. This doesn't work well with EXWM, so I disable that feature.

    (use-package ediff
      :defer t
      :custom
      (ediff-window-setup-function
       #'ediff-setup-windows-plain))
    
  • Smerge

    Easily merge git conflicts. The prefix is C-c ^ which works fine, but I also bind C-c n and C-c p to go to the next/previous hunk.

    (use-package smerge-mode
      :bind
      (:map smerge-mode-map
            ("C-c n" . smerge-next)
            ("C-c p" . smerge-prev)))
    

Proced

I don't use proced much in favour of list-processes (because virtually all of my processes are started from Emacs anyway) but I feel more comfortable with it opening in the same window for some reason.

(use-package proced
  :defer t
  :config
  (add-to-list 'display-buffer-alist
               '("\\`\\*Proced\\*\\'" display-buffer-same-window)))

Shells

Shells in Emacs - both shell and eshell settings are here.

Shell

I don't want the shell buffer to open a new window, so add an entry in display-buffer-alist.

(use-package shell
  :defer t
  :config
  (add-to-list 'display-buffer-alist
               '("\\`\\*shell\\*\\'" display-buffer-same-window)))

Eshell

A bunch of new eshell functions for my convenience; see their docstrings or org headings for more details.

Much of my eshell workflow is now housed in Eshell outline mode, so a few customisations have been removed recently.

(use-package eshell
  :custom
  (eshell-history-size 10000)
  (eshell-banner-message "")
  :init

  :bind
  (:map eshell-mode-map
        ("C-c r" . eshell/r))
  :config
  (require 'esh-mode)
  (defun eshell/e (&rest args)
    "Edit a file from eshell."
    (mapcar 'find-file args))
  (defun eshell/r (&optional name &rest _ignored)
    "Rename the current buffer.
  This will be (in order):
  - [eshell] the first argument
  - [interactive] numeric prefix arg
  - [interactive] read from minibuffer with non-numeric prefix arg
  - the current process
  - the TRAMP user@host
  - the current working directory

  If a buffer of the chosen name already exists, rename it
  uniquely."
    (interactive (list (let ((arg current-prefix-arg))
                         (cond
                          ((numberp arg)
                           arg)
                          (arg
                           (read-string "New name: "))))))
    (setq name
          (if (numberp name)
              ;; If NAME is a number (either from eshell or via prefix
              ;; arg), format it like eshell does.
              (format "<%d>" name)
            ;; Otherwise, add an extra space before.
            (format " %s"
                    (or
                     name
                     (let ((proc (eshell-interactive-process)))
                       (when proc
                         (process-name proc)))
                     (let ((dir (eshell/pwd)))
                       (if (string-match-p tramp-file-name-regexp dir)
                           (replace-regexp-in-string
                            ".*:\\(.*\\):.*" "\\1" dir)
                         (replace-regexp-in-string
                          abbreviated-home-dir "~/" dir)))))))
    (let ((buffer
           (concat eshell-buffer-name name)))
      (rename-buffer buffer (get-buffer buffer))))
  (defun eshell/ssh (&rest args)
    "Use tramp to move into an ssh directory.
  Usage: ssh [USER@]HOST [PATH]"
    (let ((host (car args))
          (path (or (cadr args) "")))
      (eshell/cd (format "/ssh:%s:%s" host path))))
  (defun eshell/img (&rest files)
    "Insert FILES into the buffer as images.
  If a file does not match `image-file-name-regexp', nothing
  happens."
    (dolist (file (mapcar #'expand-file-name (flatten-tree files)))
      (when (string-match-p (image-file-name-regexp) file)
        (goto-char (1- (point)))
        (insert "\n")
        (insert-image (create-image file nil nil
                                    :max-height (* 2 (/ (window-pixel-height) 3))
                                    :max-width (* 2 (/ (window-pixel-width) 3))))))
    (goto-char (point-max))
    nil)

  (defun eshell/shr (&rest files)
    "Insert FILES into the buffer as rendered HTML."
    (dolist (file (mapcar #'expand-file-name (flatten-tree files)))
      (when (string-match-p "\\.html\\'" file)
        (goto-char (1- (point)))
        (shr-insert-document
         (with-temp-buffer
           (insert-file-contents file)
           (libxml-parse-html-region (point-min) (point-max))))))
    (goto-char (point-max))
    nil)

  (defun eshell/fontify (&rest files)
    "Insert FILES into the buffer.
  Like `eshell/cat', but fontifies the text as it would be if it
  were visited normally."
    (dolist (file (mapcar #'expand-file-name (flatten-tree files)))
      (goto-char (1- (point)))
      (insert "\n")
      (insert
       (with-temp-buffer
         (insert-file-contents file)
         (setq buffer-file-name file)
         (normal-mode)
         (font-lock-ensure)
         (delete-region (1- (point-max)) (point-max))
         (set-buffer-modified-p nil)
         (buffer-string)))
      (goto-char (point-max)))
    nil)

  (defun eshell/c (&rest files)
    "My overpowered version of `eshell/cat'.
  This command show FILES as:
  - images (`eshell/img')
  - directories (`eshell/ls')
  - rendered html (`eshell/shr')
  - fontified source code (`eshell/fontify')"
    (dolist (file (mapcar (lambda (file)
                            (let ((expanded (expand-file-name file)))
                              (when (file-exists-p file)
                                expanded)))
                          (flatten-tree files)))
      (cond ((string-match-p (image-file-name-regexp) file)
             (eshell/img file))
            ((file-directory-p file)
             (eshell/ls "-lah" file))
            ((string-match-p "\\.html\\'" file)
             (eshell/shr file))
            (t
             (eshell/fontify file)))))
  (defun eshell/h (symbol-name &rest _ignored)
    "Show help for SYMBOL-NAME.
  If `helpful-symbol' is available, use it.  Otherwise, fall back
  to `describe-symbol'."
    (let ((function (if (fboundp 'helpful-symbol)
                        #'helpful-symbol
                      #'describe-symbol)))
      (funcall function (intern symbol-name))))
  (defun eshell/su (&rest args)
    (let ((user (or (car args) "root")))
      (eshell/cd
       (if (string-prefix-p "/ssh:" default-directory)
           (format (replace-regexp-in-string
                    "/ssh:\\(.*@\\)?:?+\\(.*\\):.*" ;regex
                    "/ssh:\\1\\2|sudo:%s@\\2:"    ;replacement
                    default-directory)            ;string
                   user)
         (format "/sudo:%s@localhost:" user)))))
  (defun eshell/comint (&rest args)
    "Start a comint session running ARGS"
    (let ((string (eshell-flatten-and-stringify args))
          (program (executable-find (car args)))
          (program-args (eshell-flatten-and-stringify (cdr args))))
      (switch-to-buffer
       (make-comint string
                    (or program
                        (user-error "Executable %s not found" (car args)))
                    nil
                    program-args)))))
  • Eshell functions
    • Edit a file

      Instead of opening a file with emacsclient, just edit it directly.

      (defun eshell/e (&rest args)
        "Edit a file from eshell."
        (mapcar 'find-file args))
      
    • Comint

      A wrapper to start a comint process from eshell.

      Used like so:

      comint ed ~/.bashrc
      
      (defun eshell/comint (&rest args)
        "Start a comint session running ARGS"
        (let ((string (eshell-flatten-and-stringify args))
              (program (executable-find (car args)))
              (program-args (eshell-flatten-and-stringify (cdr args))))
          (switch-to-buffer
           (make-comint string
                        (or program
                            (user-error "Executable %s not found" (car args)))
                        nil
                        program-args))))
      
    • ssh via tramp

      A simple ssh wrapper that uses tramp. ssh user@host will always be run as the current user via local ssh.

      (defun eshell/ssh (&rest args)
        "Use tramp to move into an ssh directory.
      Usage: ssh [USER@]HOST [PATH]"
        (let ((host (car args))
              (path (or (cadr args) "")))
          (eshell/cd (format "/ssh:%s:%s" host path))))
      
    • su via tramp

      A simple sudo wrapper that uses tramp. Works from remote hosts as well.

      (defun eshell/su (&rest args)
        (let ((user (or (car args) "root")))
          (eshell/cd
           (if (string-prefix-p "/ssh:" default-directory)
               (format (replace-regexp-in-string
                        "/ssh:\\(.*@\\)?:?+\\(.*\\):.*" ;regex
                        "/ssh:\\1\\2|sudo:%s@\\2:"      ;replacement
                        default-directory)              ;string
                       user)
             (format "/sudo:%s@localhost:" user)))))
      
    • Describe symbol

      A wee eshell interface to helpful-symbol. Falls back to describe-symbol if the above isn't available somehow.

      (defun eshell/h (symbol-name &rest _ignored)
        "Show help for SYMBOL-NAME.
      If `helpful-symbol' is available, use it.  Otherwise, fall back
      to `describe-symbol'."
        (let ((function (if (fboundp 'helpful-symbol)
                            #'helpful-symbol
                          #'describe-symbol)))
          (funcall function (intern symbol-name))))
      
    • Rename eshell buffer

      Rename the current eshell. Bound to C-c r, but can also be used from eshell with or without an argument.

      r "my buffer's new name"
      

      With an argument, the buffer will be renamed that argument. This is achieved interactively with a prefix argument.

      Otherwise, it will be named according to:

      • The current process
      • TRAMP user@host
      • The current working directory
      (defun eshell/r (&optional name &rest _ignored)
        "Rename the current buffer.
      This will be (in order):
      - [eshell] the first argument
      - [interactive] numeric prefix arg
      - [interactive] read from minibuffer with non-numeric prefix arg
      - the current process
      - the TRAMP user@host
      - the current working directory
      
      If a buffer of the chosen name already exists, rename it
      uniquely."
        (interactive (list (let ((arg current-prefix-arg))
                             (cond
                              ((numberp arg)
                               arg)
                              (arg
                               (read-string "New name: "))))))
        (setq name
              (if (numberp name)
                  ;; If NAME is a number (either from eshell or via prefix
                  ;; arg), format it like eshell does.
                  (format "<%d>" name)
                ;; Otherwise, add an extra space before.
                (format " %s"
                        (or
                         name
                         (let ((proc (eshell-interactive-process)))
                           (when proc
                             (process-name proc)))
                         (let ((dir (eshell/pwd)))
                           (if (string-match-p tramp-file-name-regexp dir)
                               (replace-regexp-in-string
                                ".*:\\(.*\\):.*" "\\1" dir)
                             (replace-regexp-in-string
                              abbreviated-home-dir "~/" dir)))))))
        (let ((buffer
               (concat eshell-buffer-name name)))
          (rename-buffer buffer (get-buffer buffer))))
      
    • eshell/c

      eshell/c is a super beefy function that supersedes eshell/cat. It uses the GUI to its advantage to show:

      (defun eshell/img (&rest files)
        "Insert FILES into the buffer as images.
      If a file does not match `image-file-name-regexp', nothing
      happens."
        (dolist (file (mapcar #'expand-file-name (flatten-tree files)))
          (when (string-match-p (image-file-name-regexp) file)
            (goto-char (1- (point)))
            (insert "\n")
            (insert-image (create-image file nil nil
                                        :max-height (* 2 (/ (window-pixel-height) 3))
                                        :max-width (* 2 (/ (window-pixel-width) 3))))))
        (goto-char (point-max))
        nil)
      
      (defun eshell/shr (&rest files)
        "Insert FILES into the buffer as rendered HTML."
        (dolist (file (mapcar #'expand-file-name (flatten-tree files)))
          (when (string-match-p "\\.html\\'" file)
            (goto-char (1- (point)))
            (shr-insert-document
             (with-temp-buffer
               (insert-file-contents file)
               (libxml-parse-html-region (point-min) (point-max))))))
        (goto-char (point-max))
        nil)
      
      (defun eshell/fontify (&rest files)
        "Insert FILES into the buffer.
      Like `eshell/cat', but fontifies the text as it would be if it
      were visited normally."
        (dolist (file (mapcar #'expand-file-name (flatten-tree files)))
          (goto-char (1- (point)))
          (insert "\n")
          (insert
           (with-temp-buffer
             (insert-file-contents file)
             (setq buffer-file-name file)
             (normal-mode)
             (font-lock-ensure)
             (delete-region (1- (point-max)) (point-max))
             (set-buffer-modified-p nil)
             (buffer-string)))
          (goto-char (point-max)))
        nil)
      
      (defun eshell/c (&rest files)
        "My overpowered version of `eshell/cat'.
      This command show FILES as:
      - images (`eshell/img')
      - directories (`eshell/ls')
      - rendered html (`eshell/shr')
      - fontified source code (`eshell/fontify')"
        (dolist (file (mapcar (lambda (file)
                                (let ((expanded (expand-file-name file)))
                                  (when (file-exists-p file)
                                    expanded)))
                              (flatten-tree files)))
          (cond ((string-match-p (image-file-name-regexp) file)
                 (eshell/img file))
                ((file-directory-p file)
                 (eshell/ls "-lah" file))
                ((string-match-p "\\.html\\'" file)
                 (eshell/shr file))
                (t
                 (eshell/fontify file)))))
      

IELM

A minor tweak for ielm, just binding C-c C-z to quit-window, as in slime, geiser, etc. To open up an ielm buffer, I use selime-ielm from my package selime, which opens it in a new window and is bound to C-c C-z in selime-mode.

(use-package ielm
  :defer t
  :config
  (define-key ielm-map (kbd "C-c C-z") #'quit-window))

Saving the state of Emacs

Packages that save where you were - recentf saves a list of edited files, and desktop saves a list of variables and current buffers.

Recentf

This package saves a list of recently visited files. I've had some problems with Helm not loading the recentf list, so it is done here.

(use-package recentf
  :config (recentf-load-list))

Desktop

Save list of buffers and some variables when exiting Emacs. Don't save a list of frames, that just ends up spamming me with extra frames everywhere.

(use-package desktop
  :custom
  (desktop-restore-frames nil)
  (history-delete-duplicates t)
  (desktop-save-mode t)
  :config
  (add-to-list 'desktop-globals-to-save 'helm-ff-history)
  (add-to-list 'desktop-globals-to-save 'extended-command-history))

Save Place

Like desktop-save-mode, but saves the place in buffers between Emacs sessions, rather than the list of buffers.

(use-package saveplace
  :config
  (save-place-mode t))

Winner-mode

Saves window configurations so that you can use C-c <left> to undo changes in window arrangement.

(use-package winner
  :config
  (winner-mode))

Interface tweaks

Some settings for the UI of Emacs - mode-line, scroll-bar, etc.

Extraneous bars

Section for the three wasteful bars – tool bar, menu bar, and scroll bar.

  • Scroll bar

    Disable the scroll bar using customize, but set the width in case I decide to turn it on.

    (use-package scroll-bar
      :custom
      (scroll-bar-mode nil)
      (scroll-bar-width 6 t))
    
  • Menu bar

    Disable the menu bar.

    (use-package menu-bar
      :config
      (menu-bar-mode -1))
    
  • Tool bar

    Disable the tool bar.

    (use-package tool-bar
      :config
      (tool-bar-mode -1))
    

Mode-line

  • Time

    Display the current time in the mode-line, and make it use 24-hour time. I adjust the time format for the world-clock so it displays the offset from UTC/GMT, and change the list of timezones.

    (use-package time
      :custom
      (display-time-24hr-format t)
      (world-clock-time-format "%A\t%d %B %R %Z\t(%z)")
      (world-clock-list
       '(("America/Los_Angeles" "Western US")
         ("America/Chicago" "Central US")
         ("America/New_York" "Eastern US")
         ("Europe/London" "UK")
         ("Europe/Paris" "Central Europe")
         ("Asia/Calcutta" "India")
         ("Asia/Chongqing" "China")
         ("Asia/Seoul" "Korea/Japan")
         ("Australia/Canberra" "Canberra")
         ("Pacific/Auckland" "New Zealand")))
      :config
      (display-time-mode t))
    
  • Battery

    Show battery information with C-z b. Configuration for showing battery status in the mode-line is in a separate heading.

    (use-package battery
      :config
      (setq battery-mode-line-format " [%b%p%%]")
    
      (defun set-display-battery-mode-accordingly ()
        "Enable `display-battery-mode' if battery is being used.
      If connected to power, or no battery is detected, disable it."
        (if (and battery-status-function
                 (or (rassoc "discharging" (funcall battery-status-function))
                     (rassoc "Discharging" (funcall battery-status-function))))
            (display-battery-mode t)
          (display-battery-mode 0)))
    
      (advice-add 'battery :after #'set-display-battery-mode-accordingly)
      :bind
      ("C-z b" . battery)
      ("<XF86Battery>" . battery))
    
    • Battery info in mode-line

      Every time battery is called (with C-z b), check if display-battery-mode should be turned on or off.

      I also adjust battery-mode-line-format to add an extra space between the battery and time. By default, these push up against each other which I do not like.

      (setq battery-mode-line-format " [%b%p%%]")
      
      (defun set-display-battery-mode-accordingly ()
        "Enable `display-battery-mode' if battery is being used.
      If connected to power, or no battery is detected, disable it."
        (if (and battery-status-function
                 (or (rassoc "discharging" (funcall battery-status-function))
                     (rassoc "Discharging" (funcall battery-status-function))))
            (display-battery-mode t)
          (display-battery-mode 0)))
      
      (advice-add 'battery :after #'set-display-battery-mode-accordingly)
      
  • Show the column

    Show the current column in the mode-line. This is provided by the simple package.

    (use-package simple
      :config
      (column-number-mode t))
    

Indicate empty lines

This displays a bunch of little lines in the fringe where there are empty lines. I decided that I want more stuff in my fringe, and have been experimenting with it recently.

It's entirely useless in non-editing modes, so I add it only to prog-mode-hook and text-mode-hook.

The state is actually controlled by the buffer-local variable indicate-empty-lines. In order to add it to hooks, I need to define a wrapper function (although called indicate-empty-lines-mode, this function is not officially a minor mode–I just named it such for consistency's sake).

(defun indicate-empty-lines-mode (&optional arg)
  "Indicate empty lines in the fringe.
This is not actually a minor mode, just a wrapper function to set
the variable `indicate-empty-lines'.

If called interactively, enable indicaty-empty-lines-mode if ARG
is positive, and disable it if ARG is zero or negative.  If
called from Lisp, also enable the mode if ARG is omitted or nil,
and toggle it if ARG is toggle; disable the mode otherwise."
  (interactive (list (or current-prefix-arg 'toggle)))
  (setq indicate-empty-lines
        (cond ((eq arg 'toggle)
               (not indicate-empty-lines))
              ((numberp arg)
               (< 1 arg))
              (t t))))

(add-hook 'text-mode-hook #'indicate-empty-lines-mode)
(add-hook 'prog-mode-hook #'indicate-empty-lines-mode)

Keybindings

A couple of keybindings to change the way lines are displayed.

  • Line wrapping

    Simple keybinding to wrap/unwrap lines. This feature is also provided by simple.

    (use-package simple
      :bind
      ("C-c t" . toggle-truncate-lines))
    
  • Line numbers

    Display line numbers. I prefer to just use the mode-line because it doesn't slow down Emacs as much.

    (use-package display-line-numbers
      :bind
      ("C-c l" . display-line-numbers-mode))
    
  • Cycle spacing

    By default, M-SPC is bound to the less powerful just-one-space. I rebind that key to cycle-spacing, which does the same thing but on successive invocations switches between one space and no spaces. Thus, M-SPC M-SPC acts like M-\ (delete-horizontal-space)

    (use-package simple
      :bind
      ("M-SPC" . cycle-spacing))
    

Minibuffer

I set the variable enable-recursive-minibuffers to allow recursive minibuffers. e.g. M-! rm -rf C-u M-: user-emacs-directory RET RET

The library mb-depth provides a minor mode that that shows how deep you are in the minibuffer "stack".

(use-package mb-depth
  :config
  (setq enable-recursive-minibuffers t)
  (minibuffer-depth-indicate-mode))

Environment variables

Set the $EDITOR to emacsclient. Because I (almost) only use other programs from within Emacs, this works. If you don't use EXWM it would be advisable to set this in ~/.xinitrc. Also set $PAGER to cat for programs launched from Emacs, helpful with eshell because some programs automatically output to the pager.

(use-package env
  :config
  (setenv "EDITOR" "emacsclient")
  (setenv "PAGER" "cat"))

Windows

Libraries related to Emacs windows. Not to be confused with the operating system.

Window

bury-buffer is a very useful function so I bind it to C-z C-z, a pretty accessible key.

For purely pedantic reasons, I also bind C-x _ to shrink-window. Why does shrink-window-horizontally have a keybinding by default but shrink-window doesn't?

A further useful keybinding is for quit-window, which sometimes isn't bound even when it should be. I bind it to s-DEL.

I set the variable switch-to-prev-buffer-skip to a custom function, which means that switch-to-prev/next-buffer and bury-buffer won't switch to a buffer that I consider boring. This includes:

  • helm, helpful, help buffers
  • empty buffers (but not exwm buffers)
(use-package window
  :no-require
  :demand
  :bind
  ("C-z C-z" . bury-buffer)
  ("s-z" . bury-buffer)
  ("C-x _" . shrink-window)
  ("<s-backspace>" . quit-window)
  ("s-s" . next-buffer)
  ("s-d" . previous-buffer)
  :config
  (defun skip-boring-buffer-please (_window buffer _bury-or-kill)
    "Return non-nil if BUFFER is boring.
A buffer is \"boring\" if one of the following is true:
- it is in `helm-major-mode', `helpful-mode', or `help-mode'
- it is empty
- it is _not_ in `exwm-mode'"
    (or (member (buffer-local-value 'major-mode buffer)
                '(helm-major-mode
                  helpful-mode
                  help-mode))
        (unless
            (equal (buffer-local-value 'major-mode buffer)
                   'exwm-mode)
          (with-current-buffer buffer
            (= (point-min) (point-max))))))
  (setq switch-to-prev-buffer-skip
        #'skip-boring-buffer-please))

Windmove

Bind s-{c,h,t,n} to switch window more easily. I use dvorak, so this is like {i,j,k,l} on a qwerty keyboard. The shifted keys swap rather than moving.

(use-package windmove
  :defer t
  :bind
  ("s-c" . windmove-up)
  ("s-h" . windmove-left)
  ("s-t" . windmove-down)
  ("s-n" . windmove-right)
  ("s-C" . windmove-swap-states-up)
  ("s-H" . windmove-swap-states-left)
  ("s-T" . windmove-swap-states-down)
  ("s-N" . windmove-swap-states-right))

Tab-bar

I've started using tab-bar-mode instead of exwm workspaces. I don't like the tab bar to be shown all the time, so I hide it.

I also add advice to show the current tab and index in the echo area. Somewhat awkwardly, a similar message is also shown by default when tab-bar-mode is nil. I prefer my less subtle message, but I might remove this in the future – maybe show it in the mode-line instead?

The keybindings s-g and s-r move to the previous or next tab respectively, which fits well with my windmove keybindings. s-w is the default keybinding in exwm to switch workspace, so I reuse the key to switch tab.

(use-package tab-bar
  :defer t
  :custom
  (tab-bar-show nil)
  (tab-bar-close-button-show nil)
  (tab-bar-new-button-show nil)
  (tab-bar-tab-hints t)
  :bind
  ("s-g" . tab-previous)
  ("s-r" . tab-next)
  ("s-w" . tab-bar-switch-to-tab)
  :config
  (dolist (k (number-sequence 0 9))
    (bind-key (kbd (format "s-%s" k)) 'tab-bar-select-tab))
  (defadvice tab-bar-select-tab
      (after show-tab-name activate)
    "Show the tab name and index+1 in the echo area."
    (message "Switched to tab: %s (%s)"
             (propertize
              (cdr (assoc 'name (tab-bar--tab)))
              'face 'error)
             (1+ (tab-bar--current-tab-index)))))

View-mode

I like using view-mode and scroll-lock-mode is kind-of useless, so I rebind ScrollLock to toggle view-mode and enable view-mode if a buffer is read-only.

Also bind some keys to simplify movement.

(use-package view
  :custom (view-read-only t)
  :bind
  ("<Scroll_Lock>" . view-mode)
  (:map view-mode-map
        ("l" . recenter-top-bottom)
        ("f" . forward-sexp)
        ("b" . backward-sexp)))

Fixing some default behaviour

Tweak some default behaviour that pisses me off.

Swap yes/no prompt with y/n

Typing yes/no is an inconvenience that can be avoided. Alias it to y/n. This would be wrapped in (use-package subr ...) but that isn't requirable.

(defalias 'yes-or-no-p 'y-or-n-p)
(bind-key "RET" 'y-or-n-p-insert-y y-or-n-p-map)

Enable all the features

Disable the annoying "This is an advanced feature" thing. It seems so dumb that this feature exists.

(use-package novice
  :custom
  (disabled-command-function nil))

Disable audible and visual bell

Don't ring the damn bell. This is provided by the file "terminal.c" which isn't a loadable feature, so use custom instead.

(use-package custom
  :custom
  (ring-bell-function 'ignore))

Theme

Allow themes to be loaded from the lisp/themes directory, allow all themes to be loaded, then load my custom theme.

I also set up timers to load my dark theme at night, and disable it in the morning. If computer is suspended when the timer is supposed to execute, it will run upon wake (not documented, had do test myself).

(use-package custom
  :custom
  (custom-theme-directory
   (expand-file-name "lisp/themes" user-emacs-directory))
  (custom-safe-themes t)
  (custom-enabled-themes '(custom))
  :config
  (run-at-time "20:00" (* 24 60 60) (lambda () (load-theme 'custom-dark)))
  (run-at-time "07:00" (* 24 60 60) (lambda () (disable-theme 'custom-dark))))

Convenience

Some convenience features.

Hippie expand

Hippie-expand is a slightly more useful replacement for dabbrev-expand. It can make use of multiple sources, including filenames, kill-ring, and dabbrev.

(use-package hippie-exp
  :defer t
  :bind
  ("M-/" . hippie-expand))

Paragraphs

Bind M-n and M-p to move by paragraph. I used to do this on a per-mode basis, but that got annoying. These functions are defined in paragraphs.el which isn't a loadable feature, so I use (use-package emacs) instead.

(use-package emacs
  :bind
  ("M-n" . forward-paragraph)
  ("M-p" . backward-paragraph))

Project

project.el is Emacs' builtin library of convenience functions for working on a "project", which is really just a directory with version control.

By default, the project-specific Eshells open in another window, so I adjust display-buffer-alist to display them in the same window.

(use-package project
  :defer t
  :config
  (add-to-list 'display-buffer-alist
               '("-eshell\\*\\'" display-buffer-same-window)))

Mail

Gnus

I've finally managed to make the switch to gnus. Frankly, my main motivation was to avoid setting up notmuch again with my university email.

As far as I can tell, using a maildir with gnus is a hassle – so I'm just using IMAP.

(use-package gnus
  :init
  (setq mail-user-agent 'gnus-user-agent)
  :config
  (setq gnus-select-method
        '(nntp "news.gwene.org"))
  (setq gnus-secondary-select-methods
        '((nnimap "gmail"
                  (nnimap-address "imap.gmail.com"))
          (nnimap "university"
                  (nnimap-address "outlook.office365.com"))
          (nnimap "mail.jamzattack.xyz")
          (nntp "news.eternal-september.org"
                (nntp-authinfo-file "~/.authinfo.gpg"))))
  (defun gnus-group-set-up-imenu-please ()
    (setq imenu-generic-expression
          '(("Topic" "\\[ \\(.*?\\) -- [0-9]+ \\]" 1)
            ("Unread" "[1-9]+.*: \\(.*\\)" 1))))
  (add-hook 'gnus-group-mode-hook 'gnus-group-set-up-imenu-please)
  (with-eval-after-load 'gnus-win
    (setq gnus-use-full-window nil))
  :bind
  ("C-z C-n" . gnus-unplugged)
  ("C-z n" . gnus-plugged))
  • Gnus-sum

    Nicer summary & thread formatting. Credit to Protesilaos Stavrou

    (use-package gnus-sum
      :defer t
      :custom
      (gnus-summary-line-format "%U%R%z %-16,16&user-date;  %4L:%-30,30f  %B%s\n")
      (gnus-summary-mode-line-format "%p")
      (gnus-sum-thread-tree-false-root "─┬> ")
      (gnus-sum-thread-tree-indent " ")
      (gnus-sum-thread-tree-leaf-with-other "├─> ")
      (gnus-sum-thread-tree-root "")
      (gnus-sum-thread-tree-single-leaf "└─> ")
      (gnus-sum-thread-tree-vertical "│")
      (gnus-ignored-from-addresses
       (mapcar #'regexp-quote
               '("jdb@jamzattack.xyz"
                 "beardsleejamie@gmail.com"
                 "beardsjami@myvuw.ac.nz"))))
    
  • Gnus-msg

    Gnus' library for sending messages. gnus-posting-styles allows you to adjust headers, signatures, etc. based on how you got to the composition buffer. All messages composed from my university mailbox will be sent from my university address. Very nice!

    Posting Styles in the gnus manual

    (use-package gnus-msg
      :defer t
      :custom
      (gnus-posting-styles
       `(("nnimap\\+university:.*"
          (From ,(format "%s <%s@%s>" user-full-name "beardsjami" "myvuw.ac.nz"))
          (signature "Jamie Beardslee (300484191)"))
         ("nnimap\\+gmail:.*"
          (From ,(format "%s <%s@%s>" user-full-name "beardsleejamie" "gmail.com")))
         ("nnimap\\+mail\\.jamzattack\\.xyz:.*"
          (From ,(format "%s <%s@%s>" user-full-name "jdb" "jamzattack.xyz"))))))
    
  • Gnus-art

    Article stuff. Gnus tries to use the smiley library to convert emoticons into images – I turned it off because it looks terrible.

    I also want some buttons to show signature status and alternative MIME types, which is achieved with gnus-buttonized-mime-types.

    (use-package gnus-art
      :defer t
      :custom
      (gnus-treat-display-smileys nil)
      (gnus-buttonized-mime-types
       '("multipart/signed" "multipart/alternative")))
    
  • Gnus-topic

    Gnus can sort your groups by topic, which I enable in gnus-group-mode-hook.

    It shows titles for empty topics by default, which I find to get in the way. I set the variable gnus-topic-display-empty-topics to disable this. Default behaviour can be restored with T H.

    (use-package gnus-topic
      :defer t
      :custom
      (gnus-topic-display-empty-topics nil)
      :hook
      (gnus-group-mode . gnus-topic-mode))
    
  • Gnus-start

    Just getting rid of a couple of extra files in $HOME.

    • Gnus by default creates ~/.newsrc in a format compatible with other newsreaders, but I don't use any so it's just an extra line in my ls.
    • Move the dribble (i.e. auto-save) files to ~/.cache.
    (use-package gnus-start
      :defer t
      :custom
      (gnus-save-newsrc-file nil)
      (gnus-dribble-directory "~/.cache/"))
    

Sendmail

Sending mail. I use msmtp to send mail because it works well with multiple smtp servers. I tried using smtpmail but couldn't get it to switch between the two easily.

I set it up to use the from header to determine how to send mail.

(use-package sendmail
  :defer t
  :config
  (setq send-mail-function 'sendmail-send-it
        sendmail-program (or (executable-find "msmtp")
                             sendmail-program)
        mail-envelope-from 'header))

Message

The mode for editing messages. I bind C-c C-q to a function that either fills or unfills the message, and C-c $ to check spelling.

(use-package message
  :config
  (defun fill-message-please (&optional unfill)
    "Fill the whole message.
  With prefix arg UNFILL, unfill the message (i.e. paragraphs will
  all be on one line)"
    (interactive "P")
    (let ((fill-column (if unfill
                           (point-max)
                         fill-column)))
      (message-fill-yanked-message)))
  (defun my-gnus-add-gcc-header ()
    "If message is from anybody@jamzattack.xyz, archive it via IMAP.
  This will also archive it in the default nnfolder+archive group."
    (interactive)
    (let ((new-gcc
           (format-time-string
            "nnfolder+archive:sent.%Y-%m,nnimap+mail.jamzattack.xyz:Sent")))
      (save-excursion
        (goto-char (point-min))
        (ignore-errors
          (when (re-search-forward "^From: \\(.*\\)jamzattack.xyz>?")
            (message-replace-header "Gcc" new-gcc))))))
  :hook
  (message-send . my-gnus-add-gcc-header)
  :bind
  (:map message-mode-map
        ("C-c C-q" . fill-message-please)
        ("C-c $" . ispell-message)))
  • Archive mail from jamzattack.xyz

    I can't figure out how to make my postfix server copy messages to "Sent", so I do it with gnus.

    (defun my-gnus-add-gcc-header ()
      "If message is from anybody@jamzattack.xyz, archive it via IMAP.
    This will also archive it in the default nnfolder+archive group."
      (interactive)
      (let ((new-gcc
             (format-time-string
              "nnfolder+archive:sent.%Y-%m,nnimap+mail.jamzattack.xyz:Sent")))
        (save-excursion
          (goto-char (point-min))
          (ignore-errors
            (when (re-search-forward "^From: \\(.*\\)jamzattack.xyz>?")
              (message-replace-header "Gcc" new-gcc))))))
    

MIME

Stuff to do with MIME

  • mm-decode

    The library responsible for decoding mime parts. I prefer reading text/plain, so discourage the other common alternatives. I also want to verify messages that have a signature, so I set mm-verify-option.

    (use-package mm-decode
      :defer t
      :custom
      (mm-discouraged-alternatives
       '("text/html" "text/richtext"))
      (mm-verify-option 'known))
    
  • mml-sec

    Yay for encryption. I set up messages to encrypt to myself as well as the recipient, and sign with the sender.

    (use-package mml-sec
      :defer t
      :custom
      (mml-secure-openpgp-encrypt-to-self t)
      (mml-secure-openpgp-sign-with-sender t))
    

Typing

Input methods

(use-package quail
  :defer t
  :config
  (push
   (cons "dvorak"
         (concat
          "                              "
          "`~1!2@3#4$5%6^7&8*9(0)[{]}    "   ; numbers
          "  '\",<.>pPyYfFgGcCrRlL/?=+\\|  " ; qwerty
          "  aAoOeEuUiIdDhHtTnNsS-_      "   ; asdf
          "  ;:qQjJkKxXbBmMwWvVzZ        "   ; zxcv
          "                              "))
   quail-keyboard-layout-alist)

  (defun toggle-quail-keyboard-layout ()
    "Toggle the keyboard layout between dvorak and qwerty.
  This sets `quail-keyboard-layout-type' to the opposite of what is
  currently selected."
    (interactive)
    (if (string-equal quail-keyboard-layout-type "dvorak")
        (quail-set-keyboard-layout "standard")
      (quail-set-keyboard-layout "dvorak"))
    (message "Switched to layout: %s"
             (propertize quail-keyboard-layout-type
                         'face 'bold)))

  (bind-key "s-\\" 'toggle-quail-keyboard-layout)
  (use-package maori-input-method
    :straight
    (maori-input-method
     :type git
     :repo "git@jamzattack.xyz:maori-input-method"))
  (use-package shavian-input-method
    :straight
    (shavian-input-method
     :type git
     :repo "git@jamzattack.xyz:shavian-input-method"))
  (with-eval-after-load "quail/hangul"
    (defun hangul2-input-method (key)
      "2-Bulsik input method."
      (setq key (quail-keyboard-translate key))
      (if (or buffer-read-only (not (alphabetp key)))
          (list key)
        (quail-setup-overlays nil)
        (let ((input-method-function nil)
              (echo-keystrokes 0)
              (help-char nil))
          (setq hangul-queue (make-vector 6 0))
          (hangul2-input-method-internal key)
          (unwind-protect
              (catch 'exit-input-loop
                (while t
                  (let* ((seq (read-key-sequence nil))
                         (cmd (lookup-key hangul-im-keymap seq))
                         key)
                    (cond
                     ((and (stringp seq)
                           (= 1 (length seq))
                           (setq key (quail-keyboard-translate (aref seq 0)))
                           (alphabetp key))
                      (hangul2-input-method-internal key))
                     ((commandp cmd)
                      (call-interactively cmd))
                     (t
                      (setq unread-command-events
                            (nconc (listify-key-sequence seq)
                                   unread-command-events))
                      (throw 'exit-input-loop nil))))))
            (quail-delete-overlays))))))
  )
  • Dvorak keyboard layout

    Define a dvorak keyboard layout and enable it.

    Quail keyboard layouts are laid out in six 30-column blocks. The first and last are above and below the alphanumeric keys. Each key is represented by a pair of its non-shifted and shifted variants i.e. aA for the a key.

    This allows me to use another input method's physical layout rather than just the keys themselves.

    In the korean-hangul input method though, characters' positions are laid out according to the physical position, so I want that to be taken into account. In other words, I want to use the "qwerty k" rather than the "dvorak k".

    Qwerty Dvorak Hangul
    k t
    r p

    Unfortunately, whether a keyboard layout actually uses this system is totally random. See the examples in the following table, where a "layout dependent" input method means that it uses the keyboard translation according to quail-keyboard-layout.

    Layout Layout dependent? Should be?
    cyrillic-translit t nil
    programmer-dvorak nil t
    korean-hangul nil t
    japanese nil nil

    Because of this mess, I also define the function toggle-quail-keyboard-layout, which switches between the two and is bound to s-\.

    (push
     (cons "dvorak"
           (concat
            "                              "
            "`~1!2@3#4$5%6^7&8*9(0)[{]}    "   ; numbers
            "  '\",<.>pPyYfFgGcCrRlL/?=+\\|  " ; qwerty
            "  aAoOeEuUiIdDhHtTnNsS-_      "   ; asdf
            "  ;:qQjJkKxXbBmMwWvVzZ        "   ; zxcv
            "                              "))
     quail-keyboard-layout-alist)
    
    (defun toggle-quail-keyboard-layout ()
      "Toggle the keyboard layout between dvorak and qwerty.
    This sets `quail-keyboard-layout-type' to the opposite of what is
    currently selected."
      (interactive)
      (if (string-equal quail-keyboard-layout-type "dvorak")
          (quail-set-keyboard-layout "standard")
        (quail-set-keyboard-layout "dvorak"))
      (message "Switched to layout: %s"
               (propertize quail-keyboard-layout-type
                           'face 'bold)))
    
    (bind-key "s-\\" 'toggle-quail-keyboard-layout)
    
  • Māori

    My own input method for Māori. It provides prefix and postfix variants.

    • Postfix:
    aa ā
    a- ā
    aaa aa
    a-- a-
    • Prefix:
    aa ā
    -a ā
    aaa aa
    –a -a

    Hosted here.

    (use-package maori-input-method
      :straight
      (maori-input-method
       :type git
       :repo "git@jamzattack.xyz:maori-input-method"))
    
  • Shavian

    My own input method for Shavian.

    Hosted here.

    (use-package shavian-input-method
      :straight
      (shavian-input-method
       :type git
       :repo "git@jamzattack.xyz:shavian-input-method"))
    
  • Hangul

    An adjustment to the hangul input method that uses quail-keyboard-translate to determine the character, rather than assuming the standard layout.

    For more information, see this section and my blog post about the subject.

    (with-eval-after-load "quail/hangul"
      (defun hangul2-input-method (key)
        "2-Bulsik input method."
        (setq key (quail-keyboard-translate key))
        (if (or buffer-read-only (not (alphabetp key)))
            (list key)
          (quail-setup-overlays nil)
          (let ((input-method-function nil)
                (echo-keystrokes 0)
                (help-char nil))
            (setq hangul-queue (make-vector 6 0))
            (hangul2-input-method-internal key)
            (unwind-protect
                (catch 'exit-input-loop
                  (while t
                    (let* ((seq (read-key-sequence nil))
                           (cmd (lookup-key hangul-im-keymap seq))
                           key)
                      (cond
                       ((and (stringp seq)
                             (= 1 (length seq))
                             (setq key (quail-keyboard-translate (aref seq 0)))
                             (alphabetp key))
                        (hangul2-input-method-internal key))
                       ((commandp cmd)
                        (call-interactively cmd))
                       (t
                        (setq unread-command-events
                              (nconc (listify-key-sequence seq)
                                     unread-command-events))
                        (throw 'exit-input-loop nil))))))
              (quail-delete-overlays))))))
    
  • Fixing various input methods

    As said above, some input methods don't work the way they should with a custom quail-keyboard-layout.

    The variable quail-package-alist is an alist of the following values:

    Index Description
    0 NAME
    1 TITLE
    2 QUAIL-MAP
    3 GUIDANCE
    4 DOCSTRING
    5 TRANSLATION-KEYS
    6 FORGET-LAST-SELECTION
    7 DETERMINISTIC
    8 KBD-TRANSLATE
    9 SHOW-LAYOUT
    10 DECODE-MAP
    11 MAXIMUM-SHORTEST
    12 OVERLAY-PLIST
    13 UPDATE-TRANSLATION-FUNCTION
    14 CONVERSION-KEYS
    15 SIMPLE

    The elements I'm mostly interested in are 8 (KBD-TRANSLATE) and 9 (SHOW-LAYOUT).

    (with-eval-after-load "quail/cyrillic"  ; no kbd-translate
      (setf (nth 8 (assoc "cyrillic-translit" quail-package-alist)) nil
            (nth 9 (assoc "cyrillic-translit" quail-package-alist)) t))
    
    (with-eval-after-load "quail/programmer-dvorak" ; kbd-translate
      (setf (nth 8 (assoc "programmer-dvorak" quail-package-alist)) t
            (nth 9 (assoc "programmer-dvorak" quail-package-alist)) t))
    

Abbrevs

(use-package abbrev
  :defer t
  :delight
  :config
  (setq save-abbrevs nil)
  (use-package text-mode-abbrevs
    :load-path "lisp/abbrev")
  )
  • My text-mode abbrevs
    (use-package text-mode-abbrevs
      :load-path "lisp/abbrev")
    

Printing

Library for printing things as postscript.

The header variables are related to the automatically generated header that shows the buffer name, file name, date, and page number. I end up disabling this feature by setting ps-print-header to nil, but nonetheless want it to look nicer in case I want to print buffer that needs pages numbers. I can do this with the function please-print-buffer-with-header defined here.

The n-up variables are for printing multiple pages on a single sheet of paper. I use this via please-print-buffer-side-by-side also defined here. I set ps-n-up-margin to 7, which is roughly 2.5mm. This allows for two 70-character wide pages to be printed side by side.

(use-package ps-print
  :defer t
  :init
  (autoload 'ps-print-preprint "ps-print")

  (defun please-print-buffer (&optional file color header side-by-side)
    "Print the current BUFFER.
  FILE is a filename to save the generated postscript in.  If this
  is provided, it will NOT be sent to the printer.

  The arguments COLOR and SIDE-BY-SIDE are straightforward -- they
  will be determined via `y-or-n-p'.

  HEADER works weirdly interactively -- I don't usually want the
  header printed so the `y-or-n-p' asks whether to remove it."
    (interactive
     (list
      ;; `ps-print-preprint' needs a list or number argument
      (ps-print-preprint (when (y-or-n-p "Save to file? ") 1))
      (y-or-n-p "Color? ")
      (not (y-or-n-p "Remove header? "))
      (y-or-n-p "Side by side? ")))
    (let* ((ps-font-size
            (if side-by-side
                '(10 . 12)
              ps-font-size))
           (ps-n-up-printing
            (if side-by-side
                2
              1))
           (ps-print-header header)
           (ps-print-color-p (if color
                                 t
                               'black-white)))
      (ps-print-buffer-with-faces file)))

  (defun please-print-buffer-side-by-side (file &optional color)
    "Print the current buffer, split into two subpages.
  This calls `ps-print-buffer-with-faces' with the variable
  `ps-n-up-printing' set to 2."
    (interactive
     (list (ps-print-preprint current-prefix-arg)
           (y-or-n-p "Color? ")))
    (please-print-buffer file color ps-print-header t))

  (defun please-print-buffer-with-header (file &optional color)
    "Print the current buffer with a header.
  This calls `ps-print-buffer-with-faces' with the variable
  `ps-print-header' set to t."
    (interactive
     (list (ps-print-preprint current-prefix-arg)
           (y-or-n-p "Color? ")))
    (please-print-buffer file color t))
  :config
  (setq ps-print-header nil
        ps-print-header-frame nil
        ps-header-lines 1
        ps-header-font-size ps-font-size
        ps-header-title-font-size ps-font-size
        ps-n-up-border-p nil
        ps-left-margin (/ (* 72 1.0) 2.54) ; 1 cm
        ps-right-margin (/ (* 72 1.0) 2.54) ; 1 cm
        ps-n-up-margin (/ (* 72 0.5) 2.54))) ; 5 mm

Printing functions

please-print-buffer is a big printing function that asks a few y-or-n-ps to determine some commonly used settings.

A couple of separate functions for invidual options are also defined: please-print-buffer-with-header and please-print-buffer-side-by-side.

I autoload ps-print-preprint rather than using require, as this goes in the :init section.

(autoload 'ps-print-preprint "ps-print")

(defun please-print-buffer (&optional file color header side-by-side)
  "Print the current BUFFER.
FILE is a filename to save the generated postscript in.  If this
is provided, it will NOT be sent to the printer.

The arguments COLOR and SIDE-BY-SIDE are straightforward -- they
will be determined via `y-or-n-p'.

HEADER works weirdly interactively -- I don't usually want the
header printed so the `y-or-n-p' asks whether to remove it."
  (interactive
   (list
    ;; `ps-print-preprint' needs a list or number argument
    (ps-print-preprint (when (y-or-n-p "Save to file? ") 1))
    (y-or-n-p "Color? ")
    (not (y-or-n-p "Remove header? "))
    (y-or-n-p "Side by side? ")))
  (let* ((ps-font-size
          (if side-by-side
              '(10 . 12)
            ps-font-size))
         (ps-n-up-printing
          (if side-by-side
              2
            1))
         (ps-print-header header)
         (ps-print-color-p (if color
                               t
                             'black-white)))
    (ps-print-buffer-with-faces file)))

(defun please-print-buffer-side-by-side (file &optional color)
  "Print the current buffer, split into two subpages.
This calls `ps-print-buffer-with-faces' with the variable
`ps-n-up-printing' set to 2."
  (interactive
   (list (ps-print-preprint current-prefix-arg)
         (y-or-n-p "Color? ")))
  (please-print-buffer file color ps-print-header t))

(defun please-print-buffer-with-header (file &optional color)
  "Print the current buffer with a header.
This calls `ps-print-buffer-with-faces' with the variable
`ps-print-header' set to t."
  (interactive
   (list (ps-print-preprint current-prefix-arg)
         (y-or-n-p "Color? ")))
  (please-print-buffer file color t))

Local packages

Not necessarily my packages, but packages that are in the lisp directory.

Internet

A selection of packages to facilitate searching and browsing the web within Emacs.

Library-genesis

My custom package for searching library genesis. I bind C-z l to a search.

Located here.

(use-package library-genesis
  :load-path "lisp/library-genesis"
  :bind
  ("C-z l" . library-genesis-search))

Reddit-browse

This is a very minimal package to ease the use of reddit within eww. It uses the old reddit mobile site, which works well with eww.

Located here.

(use-package reddit-browse
  :load-path "lisp/reddit-browse"
  :custom
  (reddit-subreddit-list '("emacs" "lisp" "lispmemes"
                           "vxjunkies" "linux" "nethack"
                           "cello" "throwers"))
  :bind
  ("C-z r" . reddit-goto-subreddit))

Toggle touchpad

A simple package I wrote to toggle the touchpad/trackpoint on my Laptops. I use pcase to adjust the device names based on the hostname.

Located here.

(use-package toggle-touchpad
  :load-path "lisp/toggle-touchpad"
  :bind
  ("<XF86TouchpadToggle>" . toggle-touchpad)
  ("C-z \\" . toggle-touchpad)
  :config
  (pcase system-name
    ("Z30C"
     (setq toggle-touchpad--trackpoint-device "AlpsPS/2 ALPS DualPoint Stick"
           toggle-touchpad--touchpad-device "AlpsPS/2 ALPS DualPoint TouchPad"))
    ("T400"
     (setq toggle-touchpad--trackpoint-device "TPPS/2 IBM TrackPoint"
           toggle-touchpad--touchpad-device "SynPS/2 Synaptics TouchPad"))))

Arch Linux settings

This file just adds a few auto-mode-alist entries for systemd and pacman files.

Located here.

(use-package arch-linux-settings
  :load-path "lisp/arch-linux-settings")

Custom EXWM config

My custom settings for EXWM - not much different from the exwm-config-default, but doesn't get in my way as much. It provides the function custom-exwm-config which is run when exwm starts.

Note: this doesn't actually start EXWM, so this needs to be done in your xinitrc.

Located here.

(use-package custom-exwm-config
  :load-path "lisp/exwm"
  :commands custom-exwm-config
  :hook
  (exwm-init . custom-exwm-config))

Miscellaneous functions

A number of functions that don't necessarily have a proper home. Bind C-c p to open the pdf output of a typesetting program, and C-h M-a to run the external "apropos" command (not to be confused with Elisp apropos).

Located here.

(use-package my-misc-defuns
  :load-path "lisp/my-misc-defuns"
  :bind
  ("C-M-\\" . indent-region-or-defun-please)
  ("C-h M-a" . system-apropos)
  ("C-c p" . open-pdf-of-current-file)
  ("C-z C-p" . jamzattack-pastebin))

Custom Helm bookmarks

This package defines a macro to create new bookmark sources, and adds a few.

Located here.

(use-package custom-helm-bookmark
  :load-path "lisp/helm"
  :after helm
  :custom
  (helm-bookmark-default-filtered-sources
   '(helm-source-bookmark-university
     helm-source-bookmark-gnus
     helm-source-bookmark-config
     helm-source-bookmark-org-misc
     helm-source-bookmark-elisp
     helm-source-bookmark-downloads
     helm-source-bookmark-magit
     helm-source-bookmark-dired
     helm-source-bookmark-info
     helm-source-bookmark-man
     helm-source-bookmark-other
     helm-source-bookmark-set)))

Minibuffer hacks

A very tiny package, just defining two functions to make the minibuffer a bit nicer.

increase-minibuffer-size-please increases the font size a bit, I add it to minibuffer-setup-hook.

exit-minibuffer-other-window exits the minibuffer in another window. This requires Emacs 28, as it uses other-window-prefix. I bind it to M-RET. e.g. M-x eww emacs M-RET will open eww in another window.

(use-package minibuffer-hacks
  :load-path "lisp/minibuffer-hacks"
  :bind
  (:map minibuffer-local-map
        ("M-RET" . exit-minibuffer-other-window))
  :hook
  (minibuffer-setup . increase-minibuffer-size-please))

Custom bitmaps

The default fringe bitmaps aren't that pretty, so I define a few of my own.

Currently, it's only:

  • left/right arrows (truncated lines)
  • left/right curly arrows (wrapped lines)
(use-package my-bitmaps
  :load-path "lisp/bitmaps"
  :hook
  (server-after-make-frame . my-bitmaps-enable)
  (window-setup . my-bitmaps-enable))

Third party packages

This is where the packages installed with straight.el are located. All of these use the :straight keyword, so that they are downloaded if they aren't already.

epkg

Since I don't use the built-in package library, epkg is a nice replacement for the UI (paired with straight for installing). It provides functions to describe, list, search by author.

One of the biggest advantages is that is shows way more information in the describe buffer, including:

  • dependencies
  • reverse dependencies
  • downloads
  • github stars
  • when last updated

It also comes with an info manual, yippee!

(use-package epkg
  :straight t
  :config
  (defun add-straight-button-to-epkg-describe (pkg &rest _ignored)
    (when (member pkg (straight-recipes-list))
      (let ((inhibit-read-only t))
        (with-current-buffer (help-buffer)
          (goto-char (point-min))
          (forward-line 1)
          (insert "\n")
          (insert-button (format "Install %s with straight"
                                 (propertize pkg 'face '(:inherit bold)))
                         'action `(lambda (&rest _ignored)
                                    (straight-use-package ',(intern pkg))))
          (insert "\n")))))

  (advice-add 'epkg-describe-package :after #'add-straight-button-to-epkg-describe)
  :bind
  ([remap describe-package] . epkg-describe-package)
  ([remap finder-by-keyword] . epkg-list-matching-packages))

"Install with straight" button

Advise epkg-describe-package to add a button for installation with straight.

(defun add-straight-button-to-epkg-describe (pkg &rest _ignored)
  (when (member pkg (straight-recipes-list))
    (let ((inhibit-read-only t))
      (with-current-buffer (help-buffer)
        (goto-char (point-min))
        (forward-line 1)
        (insert "\n")
        (insert-button (format "Install %s with straight"
                               (propertize pkg 'face '(:inherit bold)))
                       'action `(lambda (&rest _ignored)
                                  (straight-use-package ',(intern pkg))))
        (insert "\n")))))

(advice-add 'epkg-describe-package :after #'add-straight-button-to-epkg-describe)

HELM

Rebind a few keys in order to make use of Helm's features. Stuff like find-file and switch-to-buffer. Also remap C-x k to kill-this-buffer, because I use helm-mini to kill other buffers.

I also bind M-C-y to helm-show-kill-ring. I tried to use this to replace yank-pop but the latter is too engrained in my fingers.

(use-package helm
  :straight t
  :custom
  (helm-completion-style 'emacs)
  (helm-describe-variable-function 'helpful-variable)
  (helm-describe-function-function 'helpful-callable)
  (helm-show-completion-display-function
   'helm-default-display-buffer)
  (helm-buffer-max-length 24)
  (helm-split-window-preferred-function
   #'helm-split-window-please)
  (helm-ff-keep-cached-candidates nil)
  (helm-external-programs-associations
   '(("midi" . "timidity")
     ("png" . "sxiv")
     ("jpg" . "sxiv")
     ("gif" . "mpv -L")
     ("mp4" . "mpv")
     ("mkv" . "mpv")
     ("avi" . "mpv")
     ("webm" . "mpv")
     ("ps" . "zathura")
     ("pdf" . "zathura")))
  (helm-ff-cache-mode-lighter-sleep "")
  (helm-ff-cache-mode-lighter-updating "")
  :init
  (defun kill-this-buffer-please ()
    "Actually kill this buffer, unlike `kill-this-buffer' which
  sometimes doesn't work."
    (interactive)
    (kill-buffer (current-buffer)))
  :config
  (defun helm-split-window-please (window)
    "If the frame only has one window, split it.  Otherwise, select
  the largest non-exwm window."
    (if (one-window-p t)
        (split-window (selected-window) nil
                      (if (> (window-pixel-width) (window-pixel-height))
                          'right
                        'below))
      (select-window
       ;; Reworking of `get-largest-window', doesn't choose an exwm
       ;; window.
       (let ((best-size 0)
             best-window size)
         (dolist (window (window-list-1 nil 'nomini))
           (when (and (not (window-dedicated-p window))
                      (not (eq window (selected-window)))
                      (not (equal
                            (buffer-local-value
                             'major-mode (window-buffer window))
                            'exwm-mode)))
             (setq size (* (window-pixel-height window)
                           (window-pixel-width window)))
             (when (> size best-size)
               (setq best-size size)
               (setq best-window window))))
         best-window))))
  (with-eval-after-load 'helm-mode
    (dolist (f '(highlight-symbol-at-point
                 highlight-regexp
                 highlight-lines-matching-regexp
                 highlight-phrase
                 insert-char))
      (add-to-list 'helm-completing-read-handlers-alist
                   (list f))))
  (require 'helm-config)
  (delight '((helm-mode "")))
  (helm-mode t)
  :bind
  ([remap execute-extended-command] . helm-M-x)
  ("<menu><menu>" . helm-M-x)
  ("M-o" . helm-occur)
  ("s-b" . helm-mini)
  ([remap switch-to-buffer] . helm-mini)
  ("C-x k" . kill-this-buffer-please)
  ([remap find-file] . helm-find-files)
  ([remap bookmark-jump] . helm-filtered-bookmarks)
  ("M-C-y" . helm-show-kill-ring)
  (:map helm-map
        ("C-x C-t" . helm-toggle-resplit-and-swap-windows)
        ("C-t" . transpose-chars)
        ("C-h c" . describe-key-briefly)))

un-helmifying some commands

Helm provides the variable helm-completing-read-handlers-alist to determine which commands use helm for completing-read.

I disable helm for hi-lock functions, as they read a face name which is pretty slow, and insert-char.

(with-eval-after-load 'helm-mode
  (dolist (f '(highlight-symbol-at-point
               highlight-regexp
               highlight-lines-matching-regexp
               highlight-phrase
               insert-char))
    (add-to-list 'helm-completing-read-handlers-alist
                 (list f))))

Functions

  • Kill buffer

    I rebind C-x k to kill the current buffer, because helm-mini is so useful.

    (defun kill-this-buffer-please ()
      "Actually kill this buffer, unlike `kill-this-buffer' which
    sometimes doesn't work."
      (interactive)
      (kill-buffer (current-buffer)))
    
  • Split window

    The way Helm splits windows can get in the way a bit. This more predictable function selects the largest non-exwm window.

    (defun helm-split-window-please (window)
      "If the frame only has one window, split it.  Otherwise, select
    the largest non-exwm window."
      (if (one-window-p t)
          (split-window (selected-window) nil
                        (if (> (window-pixel-width) (window-pixel-height))
                            'right
                          'below))
        (select-window
         ;; Reworking of `get-largest-window', doesn't choose an exwm
         ;; window.
         (let ((best-size 0)
               best-window size)
           (dolist (window (window-list-1 nil 'nomini))
             (when (and (not (window-dedicated-p window))
                        (not (eq window (selected-window)))
                        (not (equal
                              (buffer-local-value
                               'major-mode (window-buffer window))
                              'exwm-mode)))
               (setq size (* (window-pixel-height window)
                             (window-pixel-width window)))
               (when (> size best-size)
                 (setq best-size size)
                 (setq best-window window))))
           best-window))))
    

Helm Imenu

Helm's interface to imenu. It shows more information than imenu does, and also provides a way to access an imenu for multiple buffers.

(use-package helm-imenu
  :straight helm
  :defer t
  :bind
  ("C-c i" . helm-imenu)
  ("C-c I" . helm-imenu-in-all-buffers))

Helm man

Remap C-h C-m to helm-man-woman, a Helm interface for selecting manpages.

(use-package helm-man
  :defer t
  :straight helm
  :custom
  (man-width 80)
  :bind
  (:map help-map
        ("C-m" . helm-man-woman)))

Helm system packages

Provides an abstraction layer for viewing and installing system packages.

(use-package helm-system-packages
  :straight t
  :bind
  (:map help-map
        ("C-p" . helm-system-packages)))

Helm eww

Some Helm functions for eww. I replace all the default functions with the Helm alternatives here.

(use-package helm-eww
  :straight t
  :bind
  ("C-x r e" . helm-eww-bookmarks))

Helm org

C-c i in org-mode runs the function helm-org-in-buffer-headings.

I'm not quite sure about the mechanics, but org-mode's imenu sometimes works exactly like this (just a completing-read of all headings), and sometimes only shows the first couple.

(use-package helm-org
  :straight t
  :after org
  :bind
  (:map org-mode-map
        ("C-c i" . helm-org-in-buffer-headings)))

Helm color

The default action of helm-colors is customize-face. I rarely find it more convenient than describe-face, so I replace it.

Note: this comes with Helm.

(use-package helm-color
  :defer t
  :config
  (setf (alist-get 'action helm-source-customize-face)
        '(("Describe". (lambda (line)
                         (describe-face
                          (intern (car (split-string line))))))
          ("Customize". (lambda (line)
                          (customize-face
                           (intern (car (split-string line))))))
          ("Copy name" . (lambda (line)
                           (kill-new (car (split-string line " " t))))))))

Helm epa mode

Since quite recently ([2020-08-15 Sat 07:22], commit caf78b98) helm has included an interface for epa. I enable it, of course, not only because Helm makes for a convenient completing-read, but because the stock key/recipient selection sucks.

(use-package helm-misc
  :after helm epa
  :config
  (helm-epa-mode))

Helm Files

Helm binds C-c d to delete the selected file(s) when reading a file name. This interferes with my own keybindings, so I move it to C-c C-d.

(use-package helm-files
  :defer t
  :config
  (define-key helm-find-files-map (kbd "C-c d") nil)
  (define-key helm-find-files-map (kbd "C-c C-d") #'helm-ff-persistent-delete))

Helm grep

Appropriating a couple of keybindings from helm-find-files, having some multi-file search functions easily accessible is quite handy.

(use-package helm-grep
  :bind
  ("M-g a" . helm-do-grep-ag)
  ("M-g g" . helm-grep-do-git-grep))

Helm eshell

Helm ships with a library containing a couple of useful eshell sources – history and completion. I rebind M-r to navigate the history with helm (eshell-hist-mode binds it to just prompt for a regexp which is pretty brutal in my opinion). I also bind <tab> to helm's eshell completion command – I want to be able to use the default behaviour if I want, so I don't bind TAB (meaning that I can use the stock completion with C-i if helm is too slow).

(use-package helm-eshell
  :after (eshell em-hist)
  :bind
  (:map eshell-hist-mode-map
        ("M-r" . helm-eshell-history))
  (:map eshell-mode-map
        ("<tab>" . helm-esh-pcomplete)))

Helpful

Helpful gives a whole lot more information than describe-*. I also bind C-h SPC to helpful-at-point, just to save a keypress here and there. The :straight recipe uses my fork, which doesn't depend on f.el. (I know it's minor, but I'd rather not load the extra library).

(use-package helpful
  :straight
  (helpful :type git
           :flavor melpa
           :host gitlab
           :repo "jamzattack/helpful"
           :branch "no-f")
  :init
  (defun helpful-string (str)
    "Describe the symbol named STR.
  This uses the `helpful' library instead of `describe-*'.  If STR
  doesn't name an existing symbol, call `apropos' on it."
    (let ((symbol (intern-soft str)))
      (if symbol
          (helpful-symbol symbol)
        (apropos str))))

  ;; Org links
  (with-eval-after-load 'ol
    (advice-add 'org-link--open-help
                :override #'helpful-string))

  ;; Erc buttons
  (with-eval-after-load 'erc-button
    (advice-add 'erc-button-describe-symbol
                :override #'helpful-string))

  ;; Transient help
  (with-eval-after-load 'transient
    (advice-add 'transient--describe-function
                :override #'helpful-callable))
  :config
  (defun helpful-edit-source-temporarily ()
    "Edit the source shown in the current helpful buffer.
  This pops open a buffer with only the symbol's source, rather
  than taking you to its file.

  Works with both elisp and C source code."
    (interactive)
    (unless (derived-mode-p 'helpful-mode)
      (user-error "Not in a helpful buffer"))
    (save-excursion
      (let* ((buffer
              (get-buffer-create
               (format "*%s (source)*"
                       helpful--sym)))
             (min (progn
                    (goto-char (point-min))
                    (or (re-search-forward "^Source Code$" nil t)
                        (error "No source available"))
                    (forward-line 1)
                    (point)))
             (max (progn
                    (goto-char min)
                    (end-of-defun)
                    (point)))
             (primitive-p
              (helpful--primitive-p helpful--sym helpful--callable-p)))
        (copy-to-buffer buffer
                        min
                        max)
        (pop-to-buffer buffer)
        (if primitive-p
            (c-mode)
          (emacs-lisp-mode)))))
  (defun helpful-copy-to-kill-ring (buffer)
    "Copy the callable or variable of BUFFER to the kill ring.
  Called interactively, BUFFER is the current buffer or, with
  prefix arg, read from the minibuffer."
    (interactive (list
                  (if current-prefix-arg
                      (read-buffer "Copy symbol from buffer: "
                                   (current-buffer)
                                   t
                                   (lambda (buffer)
                                     (with-current-buffer buffer
                                       (derived-mode-p 'helpful-mode))))
                    (current-buffer))))
    (with-current-buffer buffer
      (unless (eq major-mode 'helpful-mode)
        (user-error "%s is not a helpful buffer" (buffer-name buffer)))
      (kill-new (symbol-name helpful--sym))
      (message "\"%s\" saved to kill ring." helpful--sym)))
  :bind
  (:map help-map
        ("f" . helpful-callable)
        ("v" . helpful-variable)
        ("o" . helpful-symbol)
        ("k" . helpful-key)
        ("SPC" . helpful-at-point))
  (:map helpful-mode-map
        ("e" . helpful-edit-source-temporarily)
        ("w" . helpful-copy-to-kill-ring)))

Edit source

A function that opens up a new buffer with the source shown in the current helpful buffer.

This now works with both Elisp and C source code.

(defun helpful-edit-source-temporarily ()
  "Edit the source shown in the current helpful buffer.
This pops open a buffer with only the symbol's source, rather
than taking you to its file.

Works with both elisp and C source code."
  (interactive)
  (unless (derived-mode-p 'helpful-mode)
    (user-error "Not in a helpful buffer"))
  (save-excursion
    (let* ((buffer
            (get-buffer-create
             (format "*%s (source)*"
                     helpful--sym)))
           (min (progn
                  (goto-char (point-min))
                  (or (re-search-forward "^Source Code$" nil t)
                      (error "No source available"))
                  (forward-line 1)
                  (point)))
           (max (progn
                  (goto-char min)
                  (end-of-defun)
                  (point)))
           (primitive-p
            (helpful--primitive-p helpful--sym helpful--callable-p)))
      (copy-to-buffer buffer
                      min
                      max)
      (pop-to-buffer buffer)
      (if primitive-p
          (c-mode)
        (emacs-lisp-mode)))))

Save symbol to kill ring

(defun helpful-copy-to-kill-ring (buffer)
  "Copy the callable or variable of BUFFER to the kill ring.
Called interactively, BUFFER is the current buffer or, with
prefix arg, read from the minibuffer."
  (interactive (list
                (if current-prefix-arg
                    (read-buffer "Copy symbol from buffer: "
                                 (current-buffer)
                                 t
                                 (lambda (buffer)
                                   (with-current-buffer buffer
                                     (derived-mode-p 'helpful-mode))))
                  (current-buffer))))
  (with-current-buffer buffer
    (unless (eq major-mode 'helpful-mode)
      (user-error "%s is not a helpful buffer" (buffer-name buffer)))
    (kill-new (symbol-name helpful--sym))
    (message "\"%s\" saved to kill ring." helpful--sym)))

Replacing some describe-* functions with helpful

Here I redefine and advise some functions that use the builtin describe-* functions, and make them use the helpful versions.

Currently, this affects:

(defun helpful-string (str)
  "Describe the symbol named STR.
This uses the `helpful' library instead of `describe-*'.  If STR
doesn't name an existing symbol, call `apropos' on it."
  (let ((symbol (intern-soft str)))
    (if symbol
        (helpful-symbol symbol)
      (apropos str))))

;; Org links
(with-eval-after-load 'ol
  (advice-add 'org-link--open-help
              :override #'helpful-string))

;; Erc buttons
(with-eval-after-load 'erc-button
  (advice-add 'erc-button-describe-symbol
              :override #'helpful-string))

;; Transient help
(with-eval-after-load 'transient
  (advice-add 'transient--describe-function
              :override #'helpful-callable))

Major Modes

Nov.el - epub in emacs

Read epub files in Emacs. I set this up as the default mode for epubs, and set the default width to 80 columns.

(use-package nov
  :straight t
  :custom
  (nov-text-width 80)
  (nov-variable-pitch nil)
  :mode ("\\.epub\\'" . nov-mode)
  :commands nov-bookmark-jump-handler
  :config

  :bind
  (:map nov-mode-map
        ([remap set-fill-column] . nov-set-width)))
  • Set text width in nov buffer
    (defun nov-set-width (width)
      "Set the nov rendering width to WIDTH.
    If prefix arg is a number, use it.  Otherwise, read number from
    the minibuffer."
      (interactive (list
                    (if (numberp current-prefix-arg)
                        current-prefix-arg
                      (read-number "Set width: "
                                   (- (window-width) 5)))))
      (when (derived-mode-p 'nov-mode)
        (setq nov-text-width width)
        (nov-render-document)))
    

PDF-tools

Majorly increases performance when viewing pdfs within Emacs, and provides some note-taking facilities.

(use-package pdf-tools
  :straight t
  :magic ("%PDF" . pdf-view-mode)
  :custom
  (pdf-links-browse-uri-function #'pdf-links-open-please)
  :hook
  (pdf-view-mode . auto-revert-mode)
  :config
  (defun pdf-links-open-please (uri)
    "Open \"textedit://\" links via `find-file', and jump to the
  right point.  I use this because lilypond output contains such
  links."
    (if (string-match "textedit://" uri)
        (let* ((path
                ;; get rid of textedit://
                (replace-regexp-in-string
                 "\\`textedit://" "" uri))
               (split
                (split-string path ":"))
               (file
                (url-unhex-string
                 (apply #'concat (butlast split 3))))
               (extras
                (reverse (cdr split)))
               (line
                (string-to-number (caddr extras)))
               (column
                (string-to-number (car extras)))
               (buffer (find-file-noselect file)))
          (pop-to-buffer buffer)
          (goto-char (point-min))
          (forward-line (1- line))
          (move-to-column column))
      (pdf-links-browse-uri-default uri)))
  (pdf-tools-install))
  • Custom link handler

    Awkward hacky workaround to get LilyPond's links to open properly.

    (defun pdf-links-open-please (uri)
      "Open \"textedit://\" links via `find-file', and jump to the
    right point.  I use this because lilypond output contains such
    links."
      (if (string-match "textedit://" uri)
          (let* ((path
                  ;; get rid of textedit://
                  (replace-regexp-in-string
                   "\\`textedit://" "" uri))
                 (split
                  (split-string path ":"))
                 (file
                  (url-unhex-string
                   (apply #'concat (butlast split 3))))
                 (extras
                  (reverse (cdr split)))
                 (line
                  (string-to-number (caddr extras)))
                 (column
                  (string-to-number (car extras)))
                 (buffer (find-file-noselect file)))
            (pop-to-buffer buffer)
            (goto-char (point-min))
            (forward-line (1- line))
            (move-to-column column))
        (pdf-links-browse-uri-default uri)))
    

LilyPond-mode

I mirror the lilypond-mode source on my git server, in case I need to use it on a system where lilypond isn't installed.

Located here.

(use-package lilypond-mode
  :straight
  (lilypond-mode :type git
                 :repo "git@jamzattack.xyz:lilypond-mode.git")
  :delight
  (LilyPond-mode "ly" :major)
  :init
  (defalias 'lilypond-mode 'LilyPond-mode)
  (defun custom-lilypond-setup ()
    "Sets a bunch of things up for `LilyPond-mode'."
    (interactive)
    (hack-local-variables)
    (unless (or (file-exists-p "Makefile")
                (local-variable-p 'compile-command (current-buffer)))
      (setq-local compile-command
                  (format "lilypond \"%s\"" buffer-file-name)))
    (setq-local comment-column 0)
    (setq-local imenu-generic-expression
                '(("Bar" "^% bar \\([0-9]+\\)" 1)
                  ("Page" "^% PAGE \\([A-Z0-9]+\\)" 1)
                  ("Movement" "^% \\([Mm]o?ve?m?e?n?t\\) \\([A-Za-z0-9]+\\)" 2)
                  ("TODO" "^%?.*TODO[: ]?*\\(.*\\)" 1)
                  ("Variables" "^\\([a-zA-Z]+\\) *=" 1)))
    (when (fboundp 'display-fill-column-indicator-mode)
      (setq fill-column 80)
      (display-fill-column-indicator-mode)))
  :config
  (defun lilypond-insert-repeat (&optional beg end)
    "Insert a repeat around BEG and END.
  Interactively, this is the selected region.  Prompt for the
  type (volta or unfold) and number."
    (interactive "*r")
    (setq beg (copy-marker (or beg (point)))
          end (copy-marker (or end (1+ (point)))))
    (save-excursion
      (goto-char beg)
      (insert (format "\\repeat %s %s {\n"
                      (completing-read "Repeat type: " '("volta" "unfold"))
                      (read-number "Repeat number: " 2)))
      (goto-char end)
      (insert "}\n")
      (indent-region beg (1+ end))))

  (define-key LilyPond-mode-map (kbd "C-c C-r") #'lilypond-insert-repeat)
  (defun lilypond-insert-tuplet (fraction)
    "Insert a tuplet form at point.
  Prompt for FRACTION from the minibuffer.  If no text is entered,
  assume 3/2."
    (interactive "sTuplet fraction (default 3/2): ")
    (insert (format "\\tuplet %s {  }"
                    (if (string-empty-p fraction)
                        "3/2"
                      fraction)))
    (backward-char 2))

  (define-key LilyPond-mode-map (kbd "C-c C-t") #'lilypond-insert-tuplet)
  :custom
  (LilyPond-midi-command "timidity -Oj")
  (LilyPond-all-midi-command "timidity -Oj -ia")
  :mode ("\\.ly\\'" . LilyPond-mode)
  :hook (LilyPond-mode . custom-lilypond-setup))
  • Custom lilypond setup

    A few miscellaneous things to add to LilyPond-mode-hook.

    (defun custom-lilypond-setup ()
      "Sets a bunch of things up for `LilyPond-mode'."
      (interactive)
      (hack-local-variables)
      (unless (or (file-exists-p "Makefile")
                  (local-variable-p 'compile-command (current-buffer)))
        (setq-local compile-command
                    (format "lilypond \"%s\"" buffer-file-name)))
      (setq-local comment-column 0)
      (setq-local imenu-generic-expression
                  '(("Bar" "^% bar \\([0-9]+\\)" 1)
                    ("Page" "^% PAGE \\([A-Z0-9]+\\)" 1)
                    ("Movement" "^% \\([Mm]o?ve?m?e?n?t\\) \\([A-Za-z0-9]+\\)" 2)
                    ("TODO" "^%?.*TODO[: ]?*\\(.*\\)" 1)
                    ("Variables" "^\\([a-zA-Z]+\\) *=" 1)))
      (when (fboundp 'display-fill-column-indicator-mode)
        (setq fill-column 80)
        (display-fill-column-indicator-mode)))
    
  • Fancy repeat command

    Nice little command to insert a repeat. Wraps the region in \repeat and prompts for the type and number.

    (defun lilypond-insert-repeat (&optional beg end)
      "Insert a repeat around BEG and END.
    Interactively, this is the selected region.  Prompt for the
    type (volta or unfold) and number."
      (interactive "*r")
      (setq beg (copy-marker (or beg (point)))
            end (copy-marker (or end (1+ (point)))))
      (save-excursion
        (goto-char beg)
        (insert (format "\\repeat %s %s {\n"
                        (completing-read "Repeat type: " '("volta" "unfold"))
                        (read-number "Repeat number: " 2)))
        (goto-char end)
        (insert "}\n")
        (indent-region beg (1+ end))))
    
    (define-key LilyPond-mode-map (kbd "C-c C-r") #'lilypond-insert-repeat)
    
  • Fancy tuplet command
    (defun lilypond-insert-tuplet (fraction)
      "Insert a tuplet form at point.
    Prompt for FRACTION from the minibuffer.  If no text is entered,
    assume 3/2."
      (interactive "sTuplet fraction (default 3/2): ")
      (insert (format "\\tuplet %s {  }"
                      (if (string-empty-p fraction)
                          "3/2"
                        fraction)))
      (backward-char 2))
    
    (define-key LilyPond-mode-map (kbd "C-c C-t") #'lilypond-insert-tuplet)
    

Markdown

A very featureful major mode for markdown files. I only really use it for looking at READMEs though, so I add view-mode to the hook.

(use-package markdown-mode
  :straight t
  :mode "\\.md\\'"
  :hook (markdown-mode . view-mode))

GNU APL mode

I've been trying to learn a bit of APL recently, and gnu-apl-mode is an excellent way to get into it. It tries to use the super modifier to insert special characters, but I use it for my own functions so I set the prefix to ". ".

(use-package gnu-apl-mode
  :straight t
  :mode
  "\\.apl'"
  :custom
  (gnu-apl-interactive-mode-map-prefix ". ")
  (gnu-apl-mode-map-prefix ". "))

Programming

Geiser

Interact with scheme in a powerful and emacsy way. I set the scheme program name (which isn't actually a part of geiser) to whichever scheme is installed, in order of preference.

(use-package geiser
  :straight
  (geiser :type git
          :host gitlab
          :flavor melpa
          :repo "emacs-geiser/geiser")
  :defer t
  :delight
  (scheme-mode "scm" :major)
  (geiser-repl-mode "SCM>" :major)
  (geiser-autodoc-mode)
  :hook
  (scheme-mode . geiser-mode)
  (geiser-repl-mode . paredit-mode)
  :custom
  (scheme-program-name
   (or (executable-find "guile3.0")
       (executable-find "guile")
       (executable-find "chez")
       (executable-find "mit-scheme")
       "scheme"))
  (geiser-default-implementation 'guile)
  (geiser-repl-history-filename "~/.cache/geiser/history"))
  • Geiser Implementations

    As of March 2021, geiser doesn't include any implementation-specific code, so they need to be installed separately. I mostly use Guile, but also install chez and mit just in case.

    (use-package geiser-guile
      :straight
      (geiser-guile :type git
                    :host gitlab
                    :flavor melpa
                    :repo "emacs-geiser/guile")
      :after geiser)
    
    (use-package geiser-chez
      :straight
      (geiser-chez :type git
                    :host gitlab
                    :flavor melpa
                    :repo "emacs-geiser/chez")
      :after geiser)
    
    (use-package geiser-mit
      :straight
      (geiser-mit :type git
                    :host gitlab
                    :flavor melpa
                    :repo "emacs-geiser/mit")
      :after geiser)
    

SLIME

Interact with Common Lisp in a powerful and emacsy way. I set the default Lisp program, add some fancier stuff such as a nicer REPl, and move the history file out of $HOME.

I also make slime use a local copy of the hyperspec if it exists, which can be downloaded here.

(use-package slime
  :straight t
  :delight
  (lisp-mode "cl" :major)
  (slime-repl-mode "CL>" :major)
  (slime-mode)
  (slime-autodoc-mode)
  :init
  (autoload 'slime-switch-to-output-buffer "slime-repl")
  (defun disable-slime-completion ()
    (setq slime-completion-at-point-functions
          '(slime-simple-completion-at-point)))
  :hook (slime-connected . disable-slime-completion)
  :custom
  (slime-contribs '(slime-fancy))
  (slime-repl-history-file "~/.cache/slime/history")
  (common-lisp-hyperspec-root
   (if (file-exists-p "/usr/share/doc/clhs/HyperSpec/")
       "file:///usr/share/doc/clhs/HyperSpec/"
     "http://clhs.lisp.se/"))
  (slime-auto-start 'ask)
  :bind
  (:map slime-mode-map
        ("C-c C-z" . slime-switch-to-output-buffer))
  :config
  (with-eval-after-load 'slime-repl
    (bind-key "C-c C-z" #'quit-window slime-repl-mode-map))
  (setq slime-lisp-implementations
        '((roswell ("ros" "-Q" "run"))
          (sbcl ("sbcl"))
          (ccl ("ccl"))
          (clisp ("clisp")))))

Paredit

Efficient and clever editing commands for working with s-expressions. Only fully enabled in Lisp modes, but I also define a few useful keys globally.

(use-package paredit
  :straight t
  :defer t
  :delight
  :bind
  ("M-\"" . paredit-meta-doublequote)
  ("M-(" . paredit-open-round)
  ("C-(" . paredit-backward-slurp-sexp)
  ("C-)" . paredit-forward-slurp-sexp)
  ("C-{" . paredit-backward-barf-sexp)
  ("C-}" . paredit-forward-barf-sexp)
  (:map paredit-mode-map
        ("M-R" . paredit-splice-sexp-killing-backward))
  :hook
  (emacs-lisp-mode . paredit-mode)
  (lisp-interaction-mode . paredit-mode)
  (ielm-mode . paredit-mode)
  (eval-expression-minibuffer-setup . paredit-mode)
  (lisp-mode . paredit-mode)
  (slime-repl-mode . paredit-mode)
  (scheme-mode . paredit-mode))

Elf-mode

Major mode for viewing ELF files (compiled binaries). I don't use it often, but it's nice to be able to see what a program does sometimes.

(use-package elf-mode
  :straight t
  :magic ("\dELF" . elf-mode))

Macrostep

A library that I've been using for quite some time because it's a dependency of slime. But I figure I should give it a keybinding – C-c M-e to enter macrostep-mode.

(use-package macrostep
  :straight t
  :bind
  (:map emacs-lisp-mode-map
        ("C-c M-e" . macrostep-expand)))

Regexp Expand

A neat little package that shows the regexp at point in the rx format with a nice inline interface. Kind of like macrostep.

(use-package regexp-expand
  :straight
  (regexp-expand :type git
                 :host github
                 :repo "danielmartin/regexp-expand")
  :bind
  (:map emacs-lisp-mode-map
        ("C-c M-r" . regexp-expand)))

Selime   mine

This is my package to make Elisp evaluation and documentation lookup a bit more like Slime. It's often not necessary, but sometimes I find myself using C-c C-d C-f to describe an Elisp function, etc.

Hosted here.

(use-package selime
  :straight
  (selime :type git
          :flavor melpa
          :repo "git@jamzattack.xyz:selime.git")
  :delight
  :hook (emacs-lisp-mode . selime-mode))

Lex-hl   mine

My little package to highlight lexically bound variables. It provides a minor mode to add keybindings, but I bind them in elisp mode map instead of using a hook, so that loading is deferred.

Hosted here.

(use-package lex-hl
  :straight
  (lex-hl :type git
          :flavor melpa
          :repo "git@jamzattack.xyz:lex-hl.git")
  :bind
  (:map emacs-lisp-mode-map
        ("C-c `" . lex-hl-unhighlight)
        ("C-c '" . lex-hl-top-level)
        ("C-c ," . lex-hl-prompt)
        ("C-c ." . lex-hl-nearest)))

LilyPond auto-insert   mine

My own package to handle auto-insertions for LilyPond-mode. I add it to LilyPond-mode-hook.

Hosted here.

(use-package lilypond-auto-insert
  :straight
  (lilypond-auto-insert :type git
                        :flavor melpa
                        :repo "git@jamzattack.xyz:lilypond-auto-insert.git")
  :after lilypond-mode
  :custom
  (lilypond-auto-insert-language "english")
  :bind
  (:map LilyPond-mode-map
        ("C-c a" . lilypond-auto-insert)))

Lua Mode

I don't know much lua, but when I downloaded some lua scripts for mpv I was dismayed that a lua-mode isn't included with lua itself. Anyway, I just want it for font-lock, so it only needs an entry in auto-mode-alist with use-package's :mode argument.

(use-package lua-mode
  :straight t
  :mode ("\\.lua\\'" . lua-mode))

Extra org packages

Htmlize

htmlize provides a way to turn a buffer's font-lock information into html. ox-html uses this library to colour source blocks.

(use-package htmlize
  :straight t
  :defer t)

Org web tools

This package parses a web page and transforms it into beautiful org-mode. I use it in my package plumb.

(use-package org-web-tools
  :straight t
  :defer t)

Org Elisp index   mine

My package to insert a function/variable index from an elisp file into an org buffer. See its homepage for a demo.

Hosted here.

(use-package org-el-index
  :straight
  (org-el-index :type git
                :repo "git@jamzattack.xyz:org-el-index.git")
  :after org
  :bind
  (:map org-mode-map
        ("C-c M-i" . org-el-index-file-small)))

EXWM - Emacs X Window Manager

Manipulate X windows as Emacs buffers. As mentioned earlier, you need to enable exwm (via exwm-init) when creating the Emacs frame.

(use-package exwm
  :straight
  (exwm :type git
        :host github
        :repo "ch11ng/exwm")
  :defer t)

Desktop-environment (useful with EXWM)

This package sets up volume keys, brightness keys, and a screen locker. I like i3lock, and want it to use my theme's background colour.

(use-package desktop-environment
  :straight t
  :delight
  :hook
  (exwm-init . desktop-environment-mode)
  :config
  (defun custom-screenlock-command ()
    "Change the value of `desktop-environment-screenlock-command'
  to run i3lock with the background colour of the current theme."
    (let ((color (face-attribute 'default :background)))
      (setq desktop-environment-screenlock-command
            (format "i3lock -c '%s' -n"
                    (with-temp-buffer
                      (insert (if
                                  (= (length color) 7)
                                  color
                                "#000000"))
                      (beginning-of-line)
                      (delete-char 1)
                      (buffer-string))))))
  (defadvice desktop-environment-lock-screen
      (before change-bg-color activate)
    (custom-screenlock-command))
  (desktop-environment-mode))
  • Change screenlock command based on theme colour
    (defun custom-screenlock-command ()
      "Change the value of `desktop-environment-screenlock-command'
    to run i3lock with the background colour of the current theme."
      (let ((color (face-attribute 'default :background)))
        (setq desktop-environment-screenlock-command
              (format "i3lock -c '%s' -n"
                      (with-temp-buffer
                        (insert (if
                                    (= (length color) 7)
                                    color
                                  "#000000"))
                        (beginning-of-line)
                        (delete-char 1)
                        (buffer-string))))))
    

"Applications"

Vterm

A performant terminal emulator in Emacs. Unfortunately, it still doesn't play nice with complicated things such as NetHack.

(use-package vterm
  :straight t
  :defer t
  :config
  (defun eshell/vterm (&rest args)
    "Launch a program from eshell using vterm."
    (let ((vterm-shell
           (eshell-flatten-and-stringify args)))
      (vterm))))
  • Launch a vterm from eshell

    The function eshell/vterm starts a program in vterm from eshell.

    (defun eshell/vterm (&rest args)
      "Launch a program from eshell using vterm."
      (let ((vterm-shell
             (eshell-flatten-and-stringify args)))
        (vterm)))
    

Music

The following packages all provide some interface to playing music. I create my own keymap, and bind it to s-m, so that I have somewhere to put all my music keybindings.

(defvar my-music-map (make-sparse-keymap)
  "My keymap for playing music.")

(global-set-key (kbd "s-m") my-music-map)
  • Libmpdee

    An mpd library. I use it only for random/shuffle, because MPDel doesn't support that somehow.

    (use-package libmpdee
      :straight t
      :when (executable-find "mpd")
      :bind
      (:map my-music-map
            ("z" . mpd-toggle-random)))
    
  • MPDel

    A small and flexible mpd client. I bind a bunch of keys in my music map, loosely based on mpdel-core-map because I used to just use that directly.

    (use-package mpdel
      :straight t
      :when (executable-find "mpd")
      :bind
      (:map my-music-map
            ;; Playing music
            ("SPC" . libmpdel-playback-play-pause)
            ("n" . libmpdel-playback-next)
            ("p" . libmpdel-playback-previous)
            ("f" . mpdel-song-normal-increment)
            ("b" . mpdel-song-normal-decrement)
            ;; Choosing music
            ("l" . mpdel-playlist-open)
            ("L" . mpdel-core-open-stored-playlists)
            ("a" . mpdel-core-open-albums)
            (":" . mpdel-browser-open)
            ("?" . mpdel-song-open)
            ;; Searching
            ("s s" . mpdel-core-search-by-title)
            ("s l" . mpdel-core-search-by-album)
            ("s r" . mpdel-core-search-by-artist)
            ;; Volume
            ("+" . mpdel-core-volume-increase)
            ("-" . mpdel-core-volume-decrease)))
    
  • Eradio

    A very simple internet radio player. It just prompts for a station from eradio-channels and streams it via a specified media player.

    I've started getting into electronic music, so I've added a few synth/vaporwave/lofi channels, as well as the national news and classical channels.

    (use-package eradio
      :straight t
      :config
      (setq eradio-channels
            '(("RNZ Concert" . "https://radionz-ice.streamguys.com/concert.mp3")
              ("RNZ National" . "https://radionz-ice.streamguys.com/national.mp3")
              ("Paekakariki FM" . "https://icecast.groundtruth.co.nz/paekakfm.mp3")
              ("Melancholy Corner" . "https://melancholy.xyz:8443/mp3")
              ("SG Radio" . "http://stream.laut.fm/synthesizergreatest")
              ("Chilled Cow" . "https://youtube.com/watch?v=5qap5aO4i9A")
              ("Darksynth" . "https://stream.laut.fm/darksynthradio"))
            eradio-player '("mpv" "--no-video" "--speed=1" "--no-terminal"))
      :bind
      (:map my-music-map
            ("r" . eradio-play)
            ("DEL" . eradio-stop)))
    

Transmission

An Emacs front-end for the Transmission BitTorrent daemon. In the EWW section, I bind the function transmission-add-url-at-point in eww-mode.

(use-package transmission
  :straight t
  :when (executable-find "transmission-daemon")
  :defer t
  :commands transmission-mode
  :init
  (defun transmission-add-url-at-point (url &optional directory)
    "Adds torrent if point is on a magnet or torrent link.
With prefix arg, prompt for DIRECTORY in which to download."
    (interactive (list (shr-url-at-point nil)
                       (when current-prefix-arg
                         (read-directory-name "Download in: " "~/Downloads/"))))
    (transmission-add url directory))
  (defun open-transmission-in-this-window ()
    "Open the transmission buffer in the selected window.
This also sets the buffer's default directory to ~/Downloads."
    (interactive)
    (let ((buffer (get-buffer-create "*transmission*")))
      (with-current-buffer buffer
        (unless (derived-mode-p 'transmission-mode)
          (transmission-mode)
          (revert-buffer))
        (setq default-directory "~/Downloads/"))
      (pop-to-buffer-same-window buffer)))
  :bind
  ("C-z C-t" . open-transmission-in-this-window)
  :config
  (bind-keys :map transmission-mode-map
             ("M" . transmission-move)))

Elpher

Elpher is a gopher and gemini browser for Emacs.

I add an entry in browse-url-handlers so that gopher links are opened in Elpher (this does not work from eww). This requires creating a new function which can accept the extra arguments.

(use-package elpher
  :straight t
  :defer t
  :commands elpher-go
  :bind
  (:map elpher-mode-map
        ("l" . elpher-back)
        ("t" . elpher-back-to-start)
        ("g" . elpher-reload)
        ("G" . elpher-go)
        ("w" . elpher-copy-link-url)
        ("W" . elpher-copy-current-url)
        ("v" . elpher-view-raw)
        )
  :init
  (defun elpher-go-please (url &rest _ignore)
    "Like `elpher-go', but allows extra arguments.
This is useful for `browse-url-handlers'"
    (elpher-go url))
  (with-eval-after-load 'browse-url
    (add-to-list 'browse-url-handlers
                 '("\\`\\(gopher\\|gemini\\)://" . elpher-go-please))))
  • Elpher keybindings
    ("l" . elpher-back)
    ("t" . elpher-back-to-start)
    ("g" . elpher-reload)
    ("G" . elpher-go)
    ("w" . elpher-copy-link-url)
    ("W" . elpher-copy-current-url)
    ("v" . elpher-view-raw)
    
  • Elpher org-link support   mine

    My own library to provide org-link support for elpher.

    Hosted here.

    (use-package ol-elpher
      :straight
      (ol-elpher :type git
                 :repo "git@jamzattack.xyz:ol-elpher.git")
      :after ol)
    

EBDB

EBDB is a contact management system for Emacs. BBDB is used more often, but I chose EBDB because it has plenty of documentation.

I set up ebdb-gnus and ebdb-message to activate when gnus and message are loaded, because EBDB provides integration with these libraries. By default, it gets in the way a lot – opening up buffers of contacts whenever you read or write mail.

I also prefer to keep my contacts file encrypted, so I set ebdb-sources accordingly.

(use-package ebdb
  :straight
  (:host github :repo "girzel/ebdb")
  :defer t
  :custom
  (ebdb-mua-pop-up nil)
  (ebdb-sources
   (expand-file-name
    "ebdb.gpg" user-emacs-directory))
  (ebdb-completion-display-record nil)
  :init
  (with-eval-after-load 'gnus
    (require 'ebdb-gnus))
  (with-eval-after-load 'message
    (require 'ebdb-message)))

Magit

I've finally been convinced that Magit is the one true way to use git. Currently, the config is quite simple – open magit in the selected window, and show 20 recent commits instead of 10. I also make sure that the indicators show in the fringe, because it reverts to an ellipsis when magit is loaded before the windowed frame (i.e. from desktop as a daemon).

(use-package magit
  :straight t
  :custom
  (magit-display-buffer-function
   #'magit-display-buffer-same-window-except-diff-v1)
  (magit-log-section-commit-count 20)
  :bind
  ("C-x g" . magit-status)
  :config
  (with-eval-after-load 'exwm
    (setq magit-section-visibility-indicator
          '(magit-fringe-bitmap> . magit-fringe-bitmapv))))

Appearance

Rainbow-delimiters

Minor mode that highlights parentheses well.

(use-package rainbow-delimiters
  :straight t
  :defer t
  :hook (prog-mode . rainbow-delimiters-mode))

Dimmer (dim inactive buffers)

Dims inactive buffers, so that you can more clearly see which window you're in (sometimes the mode-line just doesn't cut it).

Note: if this package ever stops working, try auto-dim-other-buffers before writing your own!

(use-package dimmer
  :straight t
  :hook
  (after-init . dimmer-mode)
  :config
  (setq dimmer-fraction 0.3)
  (dimmer-configure-org)
  (dimmer-configure-magit)
  (dimmer-configure-helm)
  (dimmer-configure-gnus))

Quality of life

Edwina

Edwina provides some rudimentary dwm emulation. The function edwina-setup-dwm-keys binds similar keys to what dwm actually uses.

(use-package edwina
  :straight t
  :defer t
  :config
  (edwina-setup-dwm-keys 'super))

Plumb   mine

A way to open URLs the way I want. I bind it to C-z d. Some commands from this package are bound in the EWW section.

Hosted here.

(use-package plumb
  :straight
  (plumb :type git
         :flavor melpa
         :repo "git@jamzattack.xyz:plumb.git")
  :bind
  ("C-z d" . plumb)
  ("C-z C-d" . plumb))

Narrow-x   mine

My own package providing a few extra narrowing commands.

Hosted here.

(use-package narrow-x
  :straight
  (narrow-x :type git
            :repo "git@jamzattack.xyz:narrow-x.git")
  :bind
  (:map narrow-map
        ("h" . narrow-to-paragraph)
        ("M-n" . narrow-to-next-paragraph)
        ("M-p" . narrow-to-prev-paragraph)
        ("DEL" . narrow-to-prev-page)
        ("SPC" . narrow-to-next-page)
        ("b" . narrow-to-prev-defun)
        ("f" . narrow-to-next-defun)))

Search-query   mine

My own search query package. It simply provides a few functions so that I don't need to use DuckDuckGo's bangs, and for websites that don't have a bang.

Unfortunately, invidio.us is due to shut down on <2020-09-01 Tue>, so I use an ever-changing mirror.

Hosted here.

(use-package search-query
  :straight
  (search-query :type git
                :repo "git@jamzattack.xyz:search-query")
  :custom
  (search-query-tpb-mirror "piratebay.live")
  (search-query-invidious-mirror "yewtu.be")
  :bind
  ("C-z a" . search-archwiki)
  ("C-z t" . search-tpb)
  ("C-z y" . search-invidious)          ; just youtube really
  ("C-z w" . search-wikipedia)
  ("C-z C-w" . search-wiktionary)
  ("C-z C-e" . search-etymonline))

Insert Date   mine

My wee package to insert the date/time in a few different formats. I define C-c d as a prefix map.

Hosted here.

(use-package insert-date
  :straight
  (insert-date :type git
               :repo "git@jamzattack.xyz:insert-date.git")
  :bind
  (:prefix "C-c d" :prefix-map insert-date-map
           ("v" . insert-date-version)
           ("d" . insert-date-only-date)
           ("t" . insert-date-only-time)
           ("l" . insert-date-locale)
           ("b" . insert-date-both)
           ("i" . insert-date-iso8601)))

Itch   mine

A simple package to switch to and create temporary buffers in a particular major mode.

Hosted here.

(use-package itch
  :straight (itch :type git
                  :repo "git@jamzattack.xyz:itch")
  :bind
  ("s-a" . itch-fundamental)
  ("s-A" . itch-switch-to-fundamental)
  ("s-u" . itch-elisp)
  ("s-U" . itch-switch-to-elisp)
  ("s-e" . itch-eshell)
  ("s-E" . itch-switch-to-eshell)
  ("s-o" . itch-org)
  ("s-O" . itch-switch-to-org))

Dired

A couple of packages that enhance dired.

Dired-async

Make dired run actions in the background. This is in the package async.

(use-package dired-async
  :straight async
  :defer t
  :config
  (dired-async-mode))

Dired-subtree

Recursively list directories and cycle like org-mode. Bind TAB to show/hide a subtree, and disable the predefined faces. Part of the dired-hacks package.

(use-package dired-subtree
  :straight dired-hacks
  :after dired
  :demand t
  :custom
  (dired-subtree-use-backgrounds nil)
  :bind
  (:map dired-mode-map
        ("TAB" . dired-subtree-cycle)))

Eshell

Eshell packages

Eshell outline mode   mine

My own package to integrate outline-minor-mode with eshell.

Hosted here.

(use-package eshell-outline
  :straight
  (eshell-outline :type git
                  :flavor melpa
                  :repo "git@jamzattack.xyz:eshell-outline.git")
  :hook (eshell-mode . eshell-outline-mode))

Fish completion

Fish completion allows eshell and shell buffers to use fish completion. I only enable it when fish is installed.

(use-package fish-completion
  :straight t
  :after eshell
  :when (executable-find "fish")
  :config
  (global-fish-completion-mode))

System-packages

System-packages allows updating, installing, and removing programs installed with your system's package manager.

(use-package system-packages
  :straight t
  :defer t)

Not really useful

Lorem Ipsum

A Lorem Ipsum generator.

(use-package lorem-ipsum
  :straight t
  :defer t)