Skip to content

DarwinAwardWinner/dotemacs

Repository files navigation

Introductory Notes

This is my Emacs configuration. Most of it is stored as Emacs Lisp source blocks in in this Org-mode file.

This file is called “config.org” and not “init.org” because during the loading process, it creates a correspondingly-named “.el” file, and there already is an “init.el” here. Note also that “README.org” is a link here.

How to install this config

In order to install this config, simply clone this repository into ~/.emacs.d/:

git clone https://github.com/DarwinAwardWinner/dotemacs.git ~/.emacs.d

If you have an old .emacs file or .emacs.d directory that you want to save, move them out of the way first so that they don’t interfere with the new config.

Slow startup on first run

The first time you start up Emacs after installing this configuration, the code in init.el will install org-mode and then use that to run the Emacs lisp code in this file, which will in turn install the many packages used by this config. This could take quite a while. Subsequent startups will skip the install process and should be relatively fast.

You can run the config in batch mode at the command line without actually launching Emacs as an editor using a command like this:

$ emacs -Q -batch -nw -l ~/.emacs.d/init.el

This will load the entire config, forcing an installation of all required packages, and you can watch the progress in your terminal. Then when you launch Emacs, it will start up much more quickly (although it will still take several seconds to start).

LICENSE

All code in this config is licensed under the GPL3 (See the =LICENSE= file in this repo or the copy included with Emacs).

Exceptions to this may be noted in cases where code has been copied from another source with a different license.

Bootstrappping from init.el

The =init.el= file does all the bootstrapping necessary to load the configuration in this Org-mode file. It is required as a separate non-Org-mode file because this config does not use the version of Org-mode included with Emacs, so the proper version of Org mode must be installed before loading this file. Otherwise, loading this file first would auto-load the built-in version of Org-mode and prevent use of the newly-installed version until Emacs was restarted.

Note that the bootstrapping code also installs the Straight package manager, which it uses to install Org mode. Here we complete the setup so Straight can be used to manage all pacakges below.

Early Init File

The =early-init.el= file contains any code that must be executed early during Emacs’ startup process.

Disable garbage collection during init

I am confident that Emacs will not run out of memory during init, so there’s no point performing many small GC passes during init. Instead, GC is disabled during init and re-enabled afterward, resulting in one big GC pass at the end.

Disable package.el in favor of Straight

This is recommended by the Straight package manager.

Enable Lexical Binding

In order to enable lexical binding in the config, this must be the first line of Emacs Lisp:

;; -*- lexical-binding: t -*-

Preliminary environment setup

This section sets up a functions and other aspects of the environment that will be needed later in the configuration, so they need to be defined/prepared up front.

Don’t ask about file locks in non-interactive sessions

If multiple Emacsen are starting up at the same time, one of them may encounter a file locked by the other. Normally, it would prompt the user about what to do, but if it is running in batch mode, this will not work. This advice tells Emacs to instead simply wait for up to 30 seconds for the file to become unlocked.

;; Needed for loop macro
(require 'cl-macs)

;; userlock.el doesn't provide a feature
(load "userlock")

(defvar lock-file-noninteractive-wait-time 30
  "Seconds to wait for a file lock before signaling `file-locked'.

If Emacs is running in batch mode, instead of asking the user
whether it should edit a locked file, it will simply wait up to
this long for the file to become unlocked.

If this variable is nil, Emacs will wait forever for a locked file
to become unlocked.")

(define-advice ask-user-about-lock
    (:around (oldfun file opponent) wait-when-noninteractive)
  "When running in batch mode, always wait for file locks to clear.

Normally, when Emacs attempts to open a file that is currently
locked by another Emacs process, it will ask the user how to
handle the conflict. However, this is not an option in batch
mode. Instead, in batch mode, Emacs will simply wait until the
file becomes unlocked and then continue. If the file is still
locked after waiting for `lock-file-noninteractive-wait-time'
seconds, a `file-locked' error will be signalled."
  (if noninteractive
      (cl-loop
       initially do (message "Waiting for %S to be unlocked." file)
       with still-locked = t
       with wait-time = lock-file-noninteractive-wait-time
       while still-locked
       ;; This function is only called after a lock was detected, so
       ;; start() by waiting 1 second before checking again.
       do (sit-for 1)
       ;; Count down from wait-time to zero, if it's non-nil
       while (or (null wait-time)
                 (< 0 (setq wait-time (1- wait-time))))
       ;; Check if the file is still locked
       for still-locked = (file-locked-p file)
       finally return
       (if still-locked
           (signal 'file-locked (list file opponent))
         ;; `t' means "steal the file", which should be fine since
         ;; it's no longer locked. But first we wait one more second.
         ;; (sit-for 1)
         t)
       )
    (funcall oldfun file opponent)))

Macros for running a function without user input

This code builds up the without-user-input macro, which is like progn except that if BODY makes any attempt to read user input, all further execution is canceled and the form returns nil (note that it does not signal an error, it simply returns).

(require 'cl-macs)

(defmacro without-minibuffer (&rest body)
  "Like `progn', but stop and return nil if BODY tries to use the minibuffer.

Also disable dialogs while evaluating BODY forms, since dialogs
are just an alternative to the minibuffer."
  (declare (indent 0))
  `(catch 'tried-to-use-minibuffer
     (minibuffer-with-setup-hook
         (lambda (&rest args) (throw 'tried-to-use-minibuffer nil))
       (let ((use-dialog-box))          ; No cheating by using dialogs instead of minibuffer
         ,@body))))

(defmacro without-functions (flist &rest body)
  "Evaluate BODY, but stop and return nil if BODY calls any of the functions named in FLIST."
  (declare (indent 1))
  (let* (;; Functions are disabled by setting their body to this
         ;; temporarily.
         (fbody
          '((&rest args) (throw 'forbidden-function nil)))
         ;; This will form the first argument to `flet'
         (function-redefinitions
          (mapcar (lambda (fname) (cons fname fbody)) flist)))
    `(catch 'forbidden-function
       (cl-flet ,function-redefinitions
         ,@body))))

(defmacro without-user-input (&rest body)
  "Like `progn', but prevent any user interaction in BODY."
  (declare (indent 0))
  `(without-functions (read-event)
     (without-minibuffer
       ;; Tell BODY that it is running noninteractively
       (let ((noninteractive t))
         ,@body))))

Start emacs server

This allows emacsclient to connect. We avoid starting the server in batch mode since there is no point in that case. We also avoid starting the server if it’s already running. We start the server as soon as possible to make it less likely that a second instance of Emacs will attempt to start because it didn’t find a server already running.

Errors are ignored in case there are two instances of Emacs running. The first Emacs will start the server, and the second will silently fail, since a server is already running.

(unless (or noninteractive (bound-and-true-p server-process))
  (condition-case err
      (without-user-input (server-start))
    (error (display-warning 'config (format "Error starting server: %S" err)))))

Tell use-package to install packages using straight

This tells use-package (and by extension req-package) to use Straight to install packages by default. See here for more information.

(setq straight-use-package-by-default t)

Install req-package

This config uses ~req-package~ to configure packages, using Straight to install them automatically. So we first need to install req-package and its dependencies (particularly ~use-package~).

(straight-use-package
 '(req-package
    :type git
    :flavor melpa
    :host github
    :repo "edvorg/req-package"
    ;; This branch contains the required fixes for el-get support
    :branch "develop"))
(require 'req-package)

Install and load basic libraries

This installs and loads the s and f packages for string and filename manipulation. These are used in various places throughout this config, so we install them right now.

(req-package f :force t)
(req-package s :force t)

Fix Default Directory

Regardless of which directory Emacs is started from, I want the initial non-file buffers such as *scratch* and *Messages* to have their default-directory set to my home directory. This code goes through all non-file buffers whose default directories are the emacs starting directory or the root directory, and changes their default directories to my home directory.

This code only runs during init. If the config is reloaded later after init, this will not run again.

(unless after-init-time
  (let ((startup-dir default-directory))
    (unless (f-same? default-directory "~")
      (dolist (buf (buffer-list))
        (ignore-errors
          (with-current-buffer buf
            (when (and (null (buffer-file-name buf))
                       (not (bound-and-true-p dired-directory))
                       (or (f-same? default-directory startup-dir)
                           (f-root? default-directory)))
              (message "Changing default dir from %s to ~/ in %s"
                       default-directory (buffer-name buf))
              (cd "~"))))))))

Define eval after init function

We define a function to defer evaluation until the end of initialization.

(defun eval-after-init (form)
  "Like `eval', but waits until after init.

During emacs initialization, this registers FORM to be evaluated
in `after-init-hook'. After initialization, this is equivalent
to `(eval FORM)'."
  (if after-init-time
      (eval form)
    ;; We don't use `add-hook' here because we want to add the form
    ;; unconditionally, even if it is a duplicate.
    (add-to-list 'after-init-hook `(lambda () ,form))))

Define macro to protect buffer modified status

This defines a macro that saves the modified status of current buffer and restores it after evaluating body.

(defmacro preserve-buffer-modified-p (&rest body)
  "Evaluate BODY, then restore buffer modified status.

This can be used to edit the contents of a buffer while telling
Emacs that the buffer is still not modified."
  (declare (indent 0))
  `(let ((bmp (buffer-modified-p)))
     (prog1
         (progn ,@body)
       (set-buffer-modified-p bmp))))

Define a macro to make “turn-on-X-mode” functions

(defmacro define-activator (mode)
  "Define `turn-on-MODE', a function that turns on MODE.

MODE is a symbol naming a function defined with
`define-minor-mode' or similar. Specifically, it should turn on
the mode when called with a positive number."
  (let ((activator-name (intern (concat "turn-on-" (symbol-name mode))))
        (docstring (format "Turn on `%s'." mode)))
    `(defun ,activator-name ()
       ,docstring
       (interactive)
       (,mode 1))))

Ensure persistence directory exists

For any code that wants to save some state to disk (e.g. undo-tree), I configure it to save its state somewhere in this directory.

(make-directory (f-join user-emacs-directory "persistence") 'recursive)

Set PATH and MANPATH from shell

My shell configuration adds a lot of things to PATH dynamically (pyenv, perlbrew, etc.), so rather than try to emulate all that logic in Emacs, we simply run a shell and tell it to print out the environment variables we care about. Then we set them in Emacs. For PATH, we also set the Elisp variable exec-path, which is not auto-updated when you modify the environment variable.

This step needs to be done early, because some later configuration items depend on having the full PATH available.

(req-package exec-path-from-shell
  :force t
  :config (exec-path-from-shell-initialize))

Fix interactive vs noninteractive shell environment issues

https://github.com/purcell/exec-path-from-shell#setting-up-your-shell-startup-files-correctly

(Note: This needs to be done on every system that I use this config on.)

Ensure define-fringe-bitmap is defined

If this function is undefined (which happens with Emacs is compiled without GUI support), git-gutter-fringe won’t load. See nschum/fringe-helper.el#5.

;; Define as a no-op if not already defined
(unless (fboundp 'define-fringe-bitmap)
  (defun define-fringe-bitmap (bitmap &rest _)
    "This is a no-op placeholder function."
    ;; Return the symbol, just like the normal function does.
    bitmap))

Package configuration

This section declares all the packages required by the config and sets up variables, key bindings, and such for some of them.

Eval the following Elisp code to re-sort the below entries (this code line is not in a source block because it is not part of the actual configuration and should not be executed upon init):

CODE (mapc (apply-partially #'org-sort-entries nil) (nreverse '(?O ?a))) CODE

adjust-parens

This allows TAB and S-TAB to increase and decrease the nesting depth (and corresponding indentation) of the current lisp expression.

(req-package adjust-parens
  :commands adjust-parens-mode
  :init
  (define-activator adjust-parens-mode)
  :hook ((lisp-interaction-mode emacs-lisp-mode) .
         turn-on-adjust-parens-mode))

amx

amx is an enhanced M-x.

(req-package amx)

anzu

Anzu mode displays the total number of matches and which one is currently highlighted while doing an isearch.

(req-package anzu)

apache-mode

This loads apache-mode and sets it up to detect the vim “syntax=apache” declaration.

(req-package apache-mode
  :mode ("/apache2/.*\\.conf\\'" . apache-mode)
  :init (progn
          (defun apache-magic-mode-detect ()
            (string-match-p "^\\s-*#.*\\bsyntax=apache\\b" (buffer-string)))
          (add-to-list 'magic-mode-alist '(apache-magic-mode-detect . apache-mode))))

apt-sources-list

(req-package apt-sources-list)

async

(req-package async)

auto-complete

Auto-complete mode provides IDE-style popup completions while editing.

(req-package auto-complete
  :init (global-auto-complete-mode 1))

auto-dim-other-buffers

This package slightly dims the background of inactive windows so as to highlight which window is currently active.

(req-package auto-dim-other-buffers)

autopair

(req-package autopair
  :config
  (autopair-global-mode 1)
  (setq autopair-skip-whitespace 'chomp)
  (setq autopair-skip-criteria 'always)
  (define-advice autopair--post-command-handler
      (:after (&rest args) realign-org-tags)
    "Re-align org-mode headline tags after doing autopair."
    (when (and (eq major-mode 'org-mode)
               (org-at-heading-p))
      (org-align-tags)))
  :defer nil)

bar-cursor

This changes the cursor from a 1-character block to a bar in between characters.

(req-package bar-cursor)

beacon

Beacon mode causes the “spotlight” to shine on the cursor whenever the window scrolls, in order to highlight the new position of the cursor.

(req-package beacon)

bind-key

(req-package bind-key)

browse-url

This binds Shift+click to open a link

(req-package browse-url
  :bind ("<s-mouse-1>" . browse-url-at-mouse))

bs (Buffer Show)

(req-package bs
  :bind ("C-x C-b" . bs-show))

buttercup

Buttercup is a testing framework that I use to test several of my Emacs Lisp packages.

(req-package buttercup
  :straight
  (buttercup
   :type git
   :flavor melpa
   :files (:defaults "bin" "buttercup-pkg.el")
   :host github
   :repo "jorgenschaefer/emacs-buttercup"
   :fork (:host github
                :repo "DarwinAwardWinner/emacs-buttercup")))

caddyfile-mode

(req-package caddyfile-mode
  :mode (("Caddyfile\\." . caddyfile-mode)
         ("caddy\\.conf\\'" . caddyfile-mode))
  :config
  (defun my-caddyfile-hook ()
    (setq-local tab-width 4)
    (setq-local indent-tabs-mode nil))
  (add-hook 'caddyfile-mode-hook #'my-caddyfile-hook))

cask

(req-package cask)
(req-package cask-mode)

centered-cursor-mode

(req-package centered-cursor-mode)

cl-lib

(req-package cl-lib)

cl-lib-highlight

This package higlights cl-lib functions and macros, and also higlights old-styls cl functions and macros in orange as a reminder not to use them.

(req-package cl-lib-highlight
  :config
  (cl-lib-highlight-initialize)
  (cl-lib-highlight-warn-cl-initialize))

cperl-mode

Replace perl-mode with cperl-mode in auto-mode-alist and interpreter-mode-alist. Also associate the “.t” extension with perl (perl test files). Last, define a keyboard shortcut for cperl-perldoc.

(req-package cperl-mode
  :init
  (progn
    (mapc
     (lambda (x)
       (when (eq (cdr x) 'perl-mode)
         (setcdr x 'cperl-mode)))
     auto-mode-alist)
    (mapc
     (lambda (x)
       (when (eq (cdr x) 'perl-mode)
         (setcdr x 'cperl-mode)))
     interpreter-mode-alist))
  :bind (:map cperl-mode-map
              ("C-c C-d" . cperl-perldoc))
  :mode ("\\.[tT]\\'" . cperl-mode))

creole-mode

(req-package creole-mode
  :mode (".creole\\'" . creole-mode))

crontab-mode

(req-package crontab-mode)

crux

This provides some useful utility functions, many of which are more advanced versions of existing commands.

(req-package crux
  :bind (("C-c C-e" . crux-eval-and-replace)
         ("C-x 4 t" . crux-transpose-windows)
         ([remap move-beginning-of-line] . crux-move-beginning-of-line)))

cwl-mode

https://qiita.com/tm_tn/items/6c9653847412d115bec0

;; (req-package cwl-mode)

See the flycheck config for the remainder of the config.

decide

Decide provides functions for dice rolling and similar tasks.

(req-package decide)

diminish

This hides or shortens the names of minor modes in the modeline.

The below code sets up a custom variable diminished-minor-modes to control the diminishing of modes.

(req-package diminish
  :config
  (defun diminish-undo (mode)
    "Restore mode-line display of diminished mode MODE to its minor-mode value.
Do nothing if the arg is a minor mode that hasn't been diminished.

Interactively, enter (with completion) the name of any diminished mode (a
mode that was formerly a minor mode on which you invoked M-x diminish).
To restore all diminished modes to minor status, answer `all'.
The response to the prompt shouldn't be quoted.  However, in Lisp code,
the arg must be quoted as a symbol, as in (diminish-undo 'all)."
    (interactive
     (if diminished-mode-alist
         (list (read (completing-read
                      "Restore what diminished mode: "
                      (cons (list "all")
                            (mapcar (lambda (x) (list (symbol-name (car x))))
                                    diminished-mode-alist))
                      nil t nil 'diminish-history-symbols)))
       (error "No minor modes are currently diminished.")))
    (if (eq mode 'all)
        (cl-loop for dmode in diminished-mode-alist
                 for mode-name = (car dmode)
                 do (diminish-undo mode-name))
      (let ((minor      (assq mode      minor-mode-alist))
            (diminished (assq mode diminished-mode-alist)))
        (or minor
            (error "%S is not currently registered as a minor mode" mode))
        (when diminished
          (setq diminished-mode-alist (remove diminished diminished-mode-alist))
          (setcdr minor (cdr diminished))))))

  (defun diminish-setup (symbol newlist)
    ;; Replace symbols with one-element lists, so that each element of
    ;; NEWLIST is a valid arglist for `diminish'.
    (setq newlist
          (mapcar (lambda (x) (if (listp x) x (list x)))
                  newlist))
    (set-default symbol newlist)
    ;; Un-diminish all modes
    (diminish-undo 'all)
    ;; Diminish each mode the new list
    (mapc (lambda (x)
            (unless (listp x)
              (setq x (list x)))
            (when (assq (car x) minor-mode-alist)
              (message "Diminishing %S" x)
              (diminish (car x) (cdr x))))
          newlist))

  (defcustom diminished-minor-modes '()
    "Minor modes to be diminished, and their diminished text, if any."
    :group 'diminish
    :type '(alist :key-type (symbol :tag "Mode")
                  :value-type (choice :tag "To What"
                                      (const :tag "Hide completely" "")
                                      (string :tag "Abbreviation")))
    :set 'diminish-setup)

  (defun diminish-init ()
    (diminish-setup 'diminished-minor-modes diminished-minor-modes))

  (eval-after-init
   '(diminish-init)))

dpkg-dev-el

This is a series of elisp provided by the Debian package dpkg-dev-el. Some of them have uses outside Debian, so it’s nice to have a way to install them without dpkg.

https://packages.debian.org/sid/dpkg-dev-el https://salsa.debian.org/debian/emacs-goodies-el/tree/master/elisp/dpkg-dev-el

(req-package debian-changelog-mode
  :straight
  (debian-changelog-mode
   :type git
   :flavor melpa
   :host nil
   :repo "https://salsa.debian.org/emacsen-team/dpkg-dev-el.git"
   :files ("debian-changelog-mode.el")))
(req-package debian-control-mode
  :straight
  (debian-control-mode
   :type git
   :flavor melpa
   :host nil
   :repo "https://salsa.debian.org/emacsen-team/dpkg-dev-el.git"
   :files ("debian-control-mode.el")))
(req-package debian-copyright
  :straight
  (debian-copyright
   :type git
   :flavor melpa
   :host nil
   :repo "https://salsa.debian.org/emacsen-team/dpkg-dev-el.git"
   :files ("debian-copyright.el")))
(req-package readme-debian
  :straight
  (readme-debian
   :type git
   :flavor melpa
   :host nil
   :repo "https://salsa.debian.org/emacsen-team/dpkg-dev-el.git"
   :files ("readme-debian.el")))

editorconfig

This allows Emacs to support EditorConfig files. See http://editorconfig.org/

(req-package editorconfig
  :config (editorconfig-mode 1))

ess

(req-package ess
  :require f
  :init
  ;; Ensure that TRAMP is loaded before ESS, since loading ESS before
  ;; TRAMP causes problems
  (require 'tramp)
  :config
  ;; Open R profile file in R-mode
  (add-to-list 'auto-mode-alist '("\\.Rprofile\\'" . R-mode))
  ;; (setq ess-default-style 'OWN)
  (customize-set-variable
   'ess-own-style-list
   ;; Based on (cdr (assoc 'C++ ess-style-alist))
   '((ess-indent-offset . 4)
     (ess-offset-arguments . open-delim)
     (ess-offset-arguments-newline . prev-line)
     (ess-offset-block . prev-line)
     (ess-offset-continued . straight)
     (ess-align-nested-calls "ifelse")
     (ess-align-arguments-in-calls "function[ 	]*(")
     (ess-align-continuations-in-calls . t)
     (ess-align-blocks control-flow)
     (ess-indent-from-lhs arguments)
     (ess-indent-from-chain-start . t)
     (ess-indent-with-fancy-comments)))
  ;; Don't let ESS trigger TRAMP when entering minibuffer
  (define-advice ess-r-package-auto-activate (:before-until () not-in-minibuffer)
    "Don't run this function in a minibuffer"
    (minibufferp))
  (require 'f)
  ;; When an R file is in a directory named "scripts", suggest the
  ;; parent directory as the starting directory.
  (defun my-ess-directory-function ()
    (cond (ess-directory)
          ((string= "scripts" (f-filename (f-full default-directory)))
           (f-parent default-directory))
          (t nil)))
  (setq ess-directory-function #'my-ess-directory-function))

esup

(req-package esup
  :defer t)

filelock

(req-package filelock)

flycheck

;; (req-package flycheck
;;   :require f
;;   :config
;;   ;; (or
;;   ;;  (and (stringp flycheck-cwl-schema-path)
;;   ;;       (f-exists-p flycheck-cwl-schema-path))
;;   ;;  (setq flycheck-cwl-schema-path
;;   ;;        (car (f-glob "~/.pyenv/versions/*/lib/python*/site-packages/cwltool/schemas/v1.0/CommonWorkflowLanguage.yml")))
;;   ;;  (display-warning 'init "Could not find a CWL schema. See https://qiita.com/tm_tn/items/6c9653847412d115bec0 for more info."))
;;   )

git-gutter

This package puts change indicators in the buffer fringe to indicate what parts of the file have been added, deleted, or modified since the last Git commit.

(req-package git-gutter)
(req-package git-gutter-fringe)

git-wip

Git-wip saves a hidden commit after each file is saved, thus saving a full history of all your edits since the last real commit. We need a special config to install the git-wip command-line script along with the Emacs Lisp file, and to tell Emacs where to find the script.

(req-package git-wip-mode
  :straight
  (git-wip-mode
   :type git
   :host github
   :repo "bartman/git-wip"
   :files ("emacs/git-wip-mode.el" "git-wip"))
  :config
  (setq git-wip-path
        (f-join (f-dirname (locate-library "git-wip-mode"))
                "git-wip")))

header2

This automatically inserts a header into any new elisp file.

(req-package header2
  :config
  (progn
    (define-advice make-header (:after (&rest args) add-lexbind-variable)
      "Add `lexical-binding: t' to header."
      (when (eq major-mode 'emacs-lisp-mode)
        (save-excursion
          (add-file-local-variable-prop-line 'lexical-binding t))))
    (defsubst header-not-part-of-emacs ()
      "Insert line declaring that this file is not part of Emacs."
      (when (eq major-mode 'emacs-lisp-mode)
        (insert header-prefix-string "This file is NOT part of GNU Emacs.\n")))
    (defsubst header-completely-blank ()
      "Insert an empty line to file header (not even `header-prefix-string')."
      (insert "\n"))
    (setq header-copyright-notice
          (format-time-string "Copyright (C) %Y Ryan C. Thompson\n"))
    ;; Set up headers when creating an elisp file
    (add-hook 'emacs-lisp-mode-hook #'auto-make-header)
    ;; Update headers on save
    (add-hook 'write-file-hooks #'auto-update-file-header)
    ;; Override `header-eof' to not insert a separator line
    (defun header-eof ()
      "Insert comment indicating end of file."
      (goto-char (point-max))
      (insert "\n")
      (insert comment-start
              (concat (and (= 1 (length comment-start)) header-prefix-string)
                      (if (buffer-file-name)
                          (file-name-nondirectory (buffer-file-name))
                        (buffer-name))
                      " ends here"
                      (or (nonempty-comment-end) "\n"))))
    ;; Function to insert `provide' statement at end of file; This is
    ;; used in `make-header-hook'.
    (defun header-provide-statement ()
      "Insert `provide' statement."
      (save-excursion
        (goto-char (point-max))
        (insert
         (format "\n%s"
                 (pp-to-string `(provide ',(intern (f-base (buffer-file-name)))))))))
    ;; Prevent `auto-make-header' from setting the buffer modified flag
    (define-advice auto-make-header
        (:around (orig-fun &rest args) dont-set-buffer-modified)
      "Don't set the buffer modified flag."
      (preserve-buffer-modified-p
        (apply orig-fun args)))))

highlight-function-calls

(req-package highlight-function-calls
  :init
  (define-activator highlight-function-calls-mode)
  :hook ((lisp-interaction-mode emacs-lisp-mode) . turn-on-highlight-function-calls-mode))

highlight-stages

(req-package highlight-stages)

htmlize

This is needed for org-html-fontify-code.

(req-package htmlize)

ido-complete-space-or-hyphen

(req-package ido-complete-space-or-hyphen
  :straight
  (ido-complete-space-or-hyphen
   :type git
   :flavor melpa
   :host github
   :repo "DarwinAwardWinner/ido-complete-space-or-hyphen"))

ido-completing-read+

This uses my bleeding-edge branch of ido-completing-read+.

(req-package ido-completing-read+
  :straight
  (ido-completing-read+
   :type git
   :flavor melpa
   :files ("ido-completing-read+.el" "ido-completing-read+-pkg.el")
   :host github
   :repo "DarwinAwardWinner/ido-completing-read-plus"))

ido-yes-or-no

(req-package ido-yes-or-no)

image+

We only load this when image.el is loaded.

(req-package image+
  :after image)

iqa

This package provides shortcuts to quickly open the user’s init file for editing.

(req-package iqa
  :config
  (iqa-setup-default)
  (setq iqa-user-init-file (f-join user-emacs-directory "config.org")))

json-mode

(req-package json-mode)

lexbind-mode

This indicates in the mode-line for each Emacs Lisp file whether lexical binding is enabled for that file.

(req-package lexbind-mode
  :init (add-hook 'emacs-lisp-mode-hook (apply-partially #'lexbind-mode 1)))

mac-pseudo-daemon

This package allows Emacs to emulate the Mac OS behavior of staying open after the last window is closed, by creating a new window and hiding it until Emacs is reactivated.

;; (req-package mac-pseudo-daemon
;;   :straight
;;   (mac-pseudo-daemon
;;    :type git
;;    :host github
;;    :repo "DarwinAwardWinner/mac-pseudo-daemon"
;;    :branch "rewrite"
;;    :files ("mac-pseudo-daemon.el")))
(req-package pseudo-daemon
  :straight
  (pseudo-daemon
   :type git
   :host github
   :repo "DarwinAwardWinner/mac-pseudo-daemon"
   :branch "rewrite"
   :files ("pseudo-daemon.el")))

magit

This sets up magit, the Emacs Git interface.

Magit itself

The defvar suppresses Magit upgrade instructions. The magit-init advice causes Magit to display the status buffer for an new repository immediately after a git init (but only when called interactively).

(req-package magit
  ;; We override the recipe to choose the main branch
  :straight
  (magit
   :type git
   :flavor melpa
   :files ("lisp/magit*.el" "lisp/git-rebase.el" "docs/magit.texi"
           "docs/AUTHORS.md" "LICENSE" "Documentation/magit.texi"
           "Documentation/AUTHORS.md"
           (:exclude "lisp/magit-libgit.el" "lisp/magit-libgit-pkg.el"
                     "lisp/magit-section.el" "lisp/magit-section-pkg.el")
           "magit-pkg.el")
   :host github
   :repo "magit/magit"
   :branch "main")
  :bind (("C-c g" . magit-status))
  :init
  ;; This needs to be set or else magit will warn about things.
  (defvar magit-last-seen-setup-instructions "1.4.0")
  :config
  (define-advice magit-init (:after (&rest args) show-status)
    "Show the status buffer after initialization if interactive."
    (when (called-interactively-p 'interactive)
      (magit-status-setup-buffer (car args)))))

;; Magit is apparently missing an autoload for `magit-process-file',
;; which is called by a function in `post-command-hook', resulting in
;; an unusable emacs unless the autoload is added manually.
(req-package magit-process
  ;; This isn't a separate package, so don't try to install it
  :straight nil
  :require magit
  :commands (magit-process-file))

magit-filenotify

This package allows magit to refresh the status buffer whenever a file is modified. This mode causes problems on remote (TRAMP) files, so we only enable it for local files.

(req-package magit-filenotify
  :require magit
  :init
  (defun turn-on-magit-filenotify-mode-if-local ()
    (magit-filenotify-mode
     (if (file-remote-p default-directory)
         0
       1)))
  (add-hook 'magit-status-mode-hook
            'turn-on-magit-filenotify-mode-if-local))

markdown-mode

This mode is for editing Markdown files.

(req-package markdown-mode
  :mode ("\\.\\(md\\|mkdn\\)$" . markdown-mode))
;; Needed for editing code blocks in a separate buffer
(req-package edit-indirect)

edit-indirect

This is an optional dependency of markdown-mode.

(req-package edit-indirect
  :defer t)

mode-line-bell

Ring the bell by flashing the mode line of the active window.

(req-package mode-line-bell)

noflet

Noflet provides an enhanced version of flet, and more importantly, provides proper indentation support for flet-like macros.

(req-package noflet)

occur-context-resize

This package allows the user to dynamically change the number of context lines around matches in an occur-mode buffer using the plus and minus keys (and 0 key to reset)

(req-package occur-context-resize
  :init (add-hook 'occur-mode-hook 'occur-context-resize-mode))

org-mode

Main org config

The default implementation of the org-in-src-block-p function is broken and always returns nil, so we reimplement it correctly here. We also add a function to insert a new src block into an org-mode buffer.

Note that we put :straight nil here, since this code is inside an org file, so org-mode already installed and loaded by the time this code runs.

Org-mode supports asynchronous export functionality. By default, each async process loads the user’s full init file, but we don’t want that, because that would be too slow. So instead, whenever we run an async Org export job, we generate a minimal init file that just sets the load-path from this Emacs, disables backup & lock files, and then loads the appropriate Org-mode functionality. Then we tell the export process to use this file instead or the usual init file.

TODO: Advise org-store-link to prompt for a CUSTOM_ID property when used on a headline without one.

(req-package org
  :commands org-clocking-buffer
  :config
  ;; Custom src-block behaviors
  (progn
    (defun org-insert-src-block (src-code-type)
      "Insert a `SRC-CODE-TYPE' type source code block in org-mode."
      (interactive
       (let ((src-code-types
              '("emacs-lisp" "python" "C" "sh" "java" "js" "clojure" "C++" "css"
                "calc" "asymptote" "dot" "gnuplot" "ledger" "lilypond" "mscgen"
                "octave" "oz" "plantuml" "R" "sass" "screen" "sql" "awk" "ditaa"
                "haskell" "latex" "lisp" "matlab" "ocaml" "org" "perl" "ruby"
                "scheme" "sqlite")))
         (list (ido-completing-read "Source code type: " src-code-types))))
      (progn
        (newline-and-indent)
        (insert (format "#+BEGIN_SRC %s\n" src-code-type))
        (newline-and-indent)
        (insert "#+END_SRC\n")
        (forward-line -2)
        (org-edit-src-code)))
    (defun org-insert-or-edit-src-block (src-code-type &optional interactive-call)
      "Insert a source code block in org-mode or edit an existing one."
      (interactive (list nil t))
      (if (and (org-in-src-block-p)
               ;; Matching the logic in `org-edit-src-code'
               (org-src--on-datum-p (org-element-at-point)))
          (org-edit-src-code)
        (if interactive-call
            (call-interactively 'org-insert-src-block)
          (org-insert-src-block src-code-type)))))
  ;; Allow an emphasized expression to extend over 15 lines
  (progn
    (setcar (nthcdr 2 org-emphasis-regexp-components) " \t\r\n\"'")
    (setcar (nthcdr 4 org-emphasis-regexp-components) 15)
    (org-set-emph-re 'org-emphasis-regexp-components org-emphasis-regexp-components))
  ;; Enable org links that roll dice
  (progn
    (defvar roll-dice-command "roll")
    (defun roll-dice (&rest args)
      (interactive "sRoll dice: ")
      (let ((result
             (s-trim
              (shell-command-to-string
               (mapconcat #'shell-quote-argument (cons roll-dice-command args) " ")))))
        (when (called-interactively-p)
          (message result))
        result))
    (defun org-rolldice-open (path)
      (let ((spec (read-string "Roll dice: " path)))
        (message (roll-dice spec))))
    (org-link-set-parameters
     "roll"
     :follow #'org-rolldice-open
     ;; This must be a lambda so it is self-contained
     :export (lambda (link desc format) (or desc link))))
  ;; Use a minimal init file for org exporting
  (defun setup-org-export-async-init-file ()
    (interactive)
    (require 'gnus)
    (setq org-export-async-init-file (f-join user-emacs-directory ".temp-org-export-async-init.el"))
    (let ((init-comment
           "This auto-generated minimal init file is used for quick initialization of important variables in org-mode async exports, without needing to load the full config.")
          (init-form
           `(progn
              ;; Set some important variables
              ,@(cl-loop
                 for binding in
                 '((gc-cons-percentage 0.6) ; Disable GC for batch script
                   load-path
                   (create-lockfiles nil)
                   (make-backup-files nil)
                   org-emphasis-regexp-components)
                 if (symbolp binding)
                 collect (list 'setq binding (list 'quote (symbol-value binding)))
                 else
                 collect (cons 'setq binding))
              ;; Bring all customized org variables along
              ,@(cl-loop
                 for sym being the symbols
                 for symname = (symbol-name sym)
                 if (and (eq sym (indirect-variable sym))
                         (custom-variable-p sym)
                         (s-matches-p "^org\\(?:tbl\\)?-" (symbol-name sym))
                         (not (eq 'standard (custom-variable-state
                                             sym (symbol-value sym)))))
                 collect (list 'setq sym (list 'quote (symbol-value sym))))
              ;; Always going to need these
              (require 'org)
              (require 'org-compat)
              ;; This one defines a function for some reason
              (require 'org-macs)
              ;; This seems to be needed for some reason
              (defalias 'org-file-name-concat #'file-name-concat)
              (require 'ox)
              ;; (Try to) Load any org packages currently loaded in
              ;; the parent
              ,@(cl-loop
                 for feat in features
                 if (s-matches-p "^org\\(?:tbl\\)?-" (symbol-name feat))
                 collect (list 'require (list 'quote feat) nil t))
              ;; Try to copy any org-related autoloads from the parent
              ,@(cl-loop
                 for sym being the symbols
                 for symname = (symbol-name sym)
                 for symfun = (symbol-function sym)
                 if (and (autoloadp symfun)
                         (s-matches-p "^org\\(?:tbl\\)?-" (symbol-name sym)))
                 collect (list 'fset (list 'quote sym) (list 'quote symfun)))
              ;; We have to call this function after modifying the
              ;; variable (which should happen above)
              (org-set-emph-re 'org-emphasis-regexp-components org-emphasis-regexp-components)
              )))
      (with-temp-file org-export-async-init-file
        (emacs-lisp-mode)
        (insert ";; " init-comment)
        (fill-paragraph)
        (insert "\n\n" (gnus-pp-to-string init-form) "\n")
        (add-file-local-variable 'eval '(auto-revert-mode 1)))))
  (eval-after-init
   '(setup-org-export-async-init-file))
  ;; Fix an error in Emacs 28
  (define-advice org-eldoc-documentation-function (:filter-args (&rest _ignored) ignore-args)
    "Make the function accept and ignore any arguments."
    nil)
  :bind (("C-c l" . org-store-link)
         ("C-c a" . org-agenda)
         :map org-mode-map
         ("C-c C-'" . org-insert-or-edit-src-block)
         :map org-src-mode-map
         ("C-c C-'" . org-edit-src-exit)
         ("C-c C-c" . org-edit-src-exit)))

;; Need this loaded or Eldoc throws errors in org buffers. TODO: Could
;; probably be replaced with an autoload for `org-get-outline-path'
(autoload 'org-get-outline-path "org-refile")

org-contrib

This is now separated out from the main repo

(req-package org-contrib
  :require org
  :straight
  (org-contrib
   :type git
   :flavor melpa
   :host github
   :repo "emacsmirror/org-contrib")
  :defer t)
(req-package ox-extra
  ;; No install, it's just to configure one of the packages in
  ;; org-contrib
  :require org-contrib
  :straight nil
  :config
  (ox-extras-activate '(latex-header-blocks ignore-headlines)))

Stable-ish HTML anchors for org export

This code makes the anchor IDs generated when exporting org files to HTML less random. It sets the seed to a specific value before executing the export, which means that it should always generate the same anchors IDs given the same set of headlines.

(req-package ox-html
  :require org
  :straight nil
  :config
  (defmacro with-reproducible-rng (seed &rest body)
    "Execute BODY with reproducible RNG.

Before executing BODY, the random number generator will be
initialized with SEED, which should be a string (see `random').
Hence, the sequence of random numbers returned by `random' within
BODY will be reproducible. After BODY finishes, the random number
generatore will be reinitialized from system entropy, and will
therefore no longer be predictable.

\(There does not seem to be a way to save and restore a specific
RNG state, so the RNG state after executing this macro will not
be the same as it was prior.)"
    (declare (indent 1))
    `(unwind-protect
         (progn
           (random (or ,seed ""))
           ,@body)
       (random t)))

  (define-advice org-html-export-to-html (:around (orig-fun &rest args) reproducible-rng)
    "Use a reproducible RNG stream for HTML export.

This results in the same pseudo-random anchor IDs for
the same set of headlines every time."
    (with-reproducible-rng "org-html-export"
      (apply orig-fun args))))

CANCELLED org-superstar

;; (req-package org-superstar
;;   :require org
;;   :init (add-hook 'org-mode-hook (lambda () (org-superstar-mode 1))))

org-sidebar

(req-package org-sidebar
  :require org
  :bind (:map org-mode-map
              ("C-c t" . org-sidebar-tree-toggle)))

org-sticky-header

This doesn’t seem to work properly in Emacs 28.

;; (req-package org-sticky-header
;;   :require org
;;   :init
;;   (define-activator org-sticky-header-mode)
;;   :hook (org-mode . turn-on-org-sticky-header-mode))

org-table-sticky-header

;; (req-package org-table-sticky-header
;;   :require org
;;   :init
;;   (define-activator org-table-sticky-header-mode)
;;   :hook (org-mode . turn-on-org-table-sticky-header-mode))

org-appear

(req-package org-appear
  :require org
  :init
  (define-activator org-appear-mode)
  ;; Disabled by default for now because it's quite glitchy.
  ;; :hook (org-mode . turn-on-org-appear-mode)
  :config
  ;; Don't allow this function to throw errors, because that's a no-no
  ;; in `post-command-hook'.
  (define-advice org-appear--post-cmd (:around (orig-fun &rest args) demote-errors)
    (with-demoted-errors "Error during `org-appear--post-cmd': %S"
      (apply orig-fun args)))
  ;; Hide things that org-appear has made visible before running
  ;; certain functions that rely on those things being hidden.
  (defun org-appear--hide-current-elem ()
    (ignore-errors
      (when org-appear-mode
        (let ((current-elem (org-appear--current-elem)))
          (when current-elem
            (org-appear--hide-invisible current-elem))))))
  (define-advice org-appear-mode (:after (&rest args) set-pre-command-hook)
    (if org-appear-mode
        (add-hook 'pre-command-hook #'org-appear--hide-current-elem nil t)
      (remove-hook 'pre-command-hook #'org-appear--hide-current-elem t)))
  ;; (defmacro org-appear-hide-before (fun)
  ;;   `(define-advice ,fun (:before (&rest args) org-appear-hide-before)
  ;;      "Fill consistently regardless of org-appear element status."
  ;;      (org-appear--hide-current-elem)))
  ;; (org-appear-hide-before org-fill-paragraph)
  ;; (org-appear-hide-before org-table-align)
  ;; (org-appear-hide-before org-ctrl-c-ctrl-c)
  ;; (org-appear-hide-before org-table-justify-field-maybe)
  )

;; Error during ‘org-appear--post-cmd’: (user-error "Cannot move further up") [2 times]

org-special-block-extras

(when (version<= "27.1" emacs-version)
  (req-package org-special-block-extras))

org-modern

(req-package org-modern)

package-lint

This is used to check packages for package.el compliance.

(req-package package-lint)

paradox

Paradox provides an improved interface to package.el.

(req-package paradox)

polymode

This mode allows editing files with multiple major modes, such as Rmarkdown files, where some parts of the file are Markdown and others are R code.

(req-package polymode)
(req-package poly-R
  :mode ("\\.Rmd\\'" . poly-markdown+r-mode))

pretty-symbols

This package allows replacing certain words with symbols, for example replacing “lambda” with λ in Lisp code. The replacement is purely visual, and the files are saved with the original words.

(req-package pretty-symbols
  :config
  (progn
    (defun pretty-symbols-enable-if-available ()
      "Enable pretty-symbols in buffer if applicable.

If current buffer's `major-mode' has any pretty symbol
   substitution rules associated with it, then enable
   `pretty-symbols-mode', otherwise do nothing."
      (when (apply #'derived-mode-p
                  (delete-dups
                   (cl-mapcan (lambda (x) (cl-copy-list (nth 3 x)))
                              pretty-symbol-patterns)))
        (pretty-symbols-mode 1)))
    (add-hook 'after-change-major-mode-hook #'pretty-symbols-enable-if-available)))

python-mode

“.pyi” is the file extension for the Python typeshed’s type annotations. These files are valid (but incomplete) Python syntax, so regular python-mode is just fine.

(req-package python
  :mode ("\\.pyi" . python-mode))

rainbow-delimiters

(req-package rainbow-delimiters
  :init
  (add-hook 'prog-mode-hook #'rainbow-delimiters-mode-enable))

reveal-in-osx-finder

(req-package reveal-in-osx-finder)

selectrum

(req-package selectrum)
(req-package selectrum-prescient)

shrink-whitespace

(req-package shrink-whitespace
  :commands shrink-whitespace)

SLIME

(req-package slime)

smooth-scrolling

(req-package smooth-scrolling
  :straight
  (smooth-scrolling
   :type git
   :flavor melpa
   :host github
   :repo "aspiers/smooth-scrolling"
   :fork (:host github
                :repo "DarwinAwardWinner/smooth-scrolling")))

snakemake

(req-package snakemake-mode)

system-specific-settings

(req-package system-specific-settings)

tempbuf

(req-package tempbuf
  :config
  (defun mode-symbol (sym)
    "Append \"-mode\" to SYM unless it already ends in it."
    (let ((symname (symbol-name sym)))
      (intern
       (concat symname
               (unless (s-suffix? "-mode" symname)
                 "-mode")))))

  (defun tempbuf-protect ()
    "Prevent tempbuf from killing visible or unsaved buffers."
    (when (or (get-buffer-window)
              (buffer-modified-p))
      (throw 'tempbuf-skip-kill nil)))
  (add-hook 'tempbuf-kill-hook 'tempbuf-protect)

  (defun tempbuf-major-mode-hook ()
    "Turn on `tempbuf-mode' in current buffer if buffer's `major-mode' is in `tempbuf-temporary-major-modes'.

Else turn off `tempbuf-mode'."
    (if (apply #'derived-mode-p tempbuf-temporary-major-modes)
        (turn-on-tempbuf-mode)
      (turn-off-tempbuf-mode)))

  (defun tempbuf-setup-temporary-major-modes (symbol newval)
    (set-default symbol (mapcar 'mode-symbol newval))
    ;; Set tempbuf-mode correctly in existing buffers.
    (mapc (lambda (buf)
            (with-current-buffer buf
              (tempbuf-major-mode-hook)))
          (buffer-list)))

  (defcustom tempbuf-temporary-major-modes nil
    "Major modes in which `tempbuf-mode' should be activated.

This will cause buffers of these modes to be automatically killed
if they are inactive for a short while."
    :group 'tempbuf
    :set 'tempbuf-setup-temporary-major-modes
    :type '(repeat (symbol :tag "Mode")))

  (add-hook 'after-change-major-mode-hook 'tempbuf-major-mode-hook)
  ;; This mode requires special handling because it somehow avoids
  ;; using `after-change-major-mode-hook', I think.
  (eval-after-load 'ess-custom
    '(add-hook 'ess-help-mode-hook 'tempbuf-major-mode-hook)))

undo-tree

(req-package undo-tree
  :config
  (require 'epa-hook)
  (define-advice undo-tree-save-history-from-hook
      (:around (orig-fun &rest args) no-save-gpg-hist)
    "Don't save history to disk for gpg files"
    (unless (and (stringp buffer-file-name)
                 (s-matches-p epa-file-name-regexp (buffer-file-name)))
      (apply orig-fun args))))

volatile-highlight

(req-package volatile-highlights
  :config
  (put 'vhl/define-extension 'lisp-indent-function 1)
  (vhl/define-extension 'adjust-parens
    'lisp-indent-adjust-parens
    'lisp-dedent-adjust-parens)
  (vhl/install-extension 'adjust-parens)
  (vhl/define-extension 'undo-tree
    'undo-tree-yank 'undo-tree-move)
  (vhl/install-extension 'undo-tree)
  ;; Clear volatile highlights after 1 second
  (setq vhl/idle-clear-timer
        (run-with-idle-timer 1 t #'vhl/clear-all)))

(define-advice vhl/add-range (:before-while (&rest args) disable-in-read-only-buffers)
  "Don't do volatile highlights in read-only buffers"
  (not buffer-read-only))

which-key

(req-package which-key
  :defer t)

with-simulated-input

(req-package with-simulated-input)

ws-butler

(req-package ws-butler)

yaml-mode

(req-package yaml-mode)

elsa

;; (req-package elsa)
;; (req-package flycheck-elsa
;;   :require flycheck elsa)

forge

Forge seems to be broken right now

;; Straight fails to auto-detect these dependencies, so we have to
;; declare them here?
(req-package emacsql-sqlite)
(req-package closql
  :require emacsql-sqlite)
(req-package ghub)
(req-package forge
  :require closql ghub)
;; I'm unsure how this fits into the dependency hierarchy, but I think
;; it's fine because it's only a dependency at runtime, so load order
;; doesn't matter.
(req-package sqlite3)

crosshairs

;; (req-package crosshairs)

More packages?

These are some packages in package-selected-packages as of the last time I used that. I need to go through and figure out which of these are incidental and which I actually wanted but forgot to configure.

  • ace-window
  • adaptive-wrap
  • aggressive-indent
  • apples-mode
  • avy
  • better-shell
  • commander
  • counsel
  • editorconfig
  • ess-smart-underscore
  • feature-mode
  • filelock
  • fireplace
  • github-browse-file
  • github-clone
  • gitignore-mode
  • guide-key
  • hardhat
  • haskell-mode
  • help-fns+
  • highlight-escape-sequences
  • ht
  • hydra
  • ido-load-library
  • isearch+
  • jedi
  • julia-mode
  • keydef
  • lexbind-mode
  • log4e
  • lv
  • macrostep
  • magit-find-file
  • md-readme
  • multiple-cursors
  • nameless
  • pcre2el
  • py-isort
  • pyenv-mode
  • restart-emacs
  • scratch-ext
  • shrink-whitespace
  • slime
  • string-edit
  • swiper
  • sx
  • systemd
  • toc-org
  • transmission
  • transpose-frame
  • treepy
  • trinary
  • ztree

Install and load all configured packages

The req-package forms above only declare the set of packages to be installed and loaded. They don’t actually do anything until the line of code below is run. At this time, req-package resolves any dependencies between packages and then installs and loads them in the correct order to satisfy those dependencies.

We use without-user-input here mainly because sometimes packages (e.g. sqlite3) want to ask the user questions during install, but they will avoid doing so if Emacs says it’s noninteractive.

(without-user-input
  (req-package-finish))

Set up and load a separate custom file

This is the file where everything set via M-x customize goes.

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(if (f-exists? custom-file)
    (load custom-file)
  (display-warning 'init "Could not load custom file"))

Tweaks

This section contains a set of tweaks to Emacs behavior that are not specific to a single package and cannot be accomplished by customizing variables.

Fixes for packages

(Currently none)

Environment tweaks

Use GNU ls for insert-directory if possible

On OS X (and probably other platforms), “ls” may not refer to GNU ls. If GNU ls is installed on these platforms, it is typically installed under the name “gls” instead. So if “gls” is available, we prefer to use it.

(if (executable-find "gls")
    (setq insert-directory-program "gls"))

Use external mailer for bug reports

This calls report-emacs-bug, then report-emacs-insert-to-mailer, then cleans up the bug buffers.

The backquoting interpolation is used to copy the interactive form from report-emacs-bug.

(eval
 `(defun report-emacs-bug-via-mailer (&rest args)
    "Report a bug in GNU Emacs.

Prompts for bug subject. Opens external mailer."
    ,(interactive-form 'report-emacs-bug)
    (save-window-excursion
      (apply 'report-emacs-bug args)
      (report-emacs-bug-insert-to-mailer)
      (mapc (lambda (buf)
              (with-current-buffer buf
                (let ((buffer-file-name nil))
                  (kill-buffer (current-buffer)))))
            (list "*Bug Help*" (current-buffer))))))

Tell Emacs where to find its C source code

This is where I keep Emacs, but you’ll probably need to edit this if you want look at the definitions of Emacs primitive functions.

(setq find-function-C-source-directory "~/src/emacs/src")

Fix Mac OS movement keys

Unswap some Command/Option shortcuts

I map Option -> Super and Command -> Meta in Emacs on Mac OS, which is the opposite of what it is by default, because I need Emacs’ meta key to be directly below X. However, there are a handful of shortcuts involving Command/Option that I don’t want swapped, so I need to swap their Super/Meta bindings to cancel out the swapping of Super and Meta themselves.

;; Use `eval-after-load' to ensure that this always happens after
;; loading custom.el, since that sets the Command/Option modifiers.
(eval-after-init
 ;; Only swap on Mac OS
 '(when (or (featurep 'ns)
            (eq system-type 'darwin))
    ;; Only swap bindings if keys were actually swapped
    (when (and (eq ns-command-modifier 'meta)
               (eq ns-option-modifier 'super))
      ;; Super is the Alt/option key
      (bind-key "s-<left>" 'left-word)
      (bind-key "s-<right>" 'right-word)
      (bind-key "s-<backspace>" 'backward-kill-word)
      (bind-key "s-<kp-delete>" 'kill-word)
      (bind-key "s-`" 'tmm-menubar)
      ;; Meta is the command key
      (bind-key "M-<left>" 'move-beginning-of-line)
      (bind-key "M-<right>" 'move-end-of-line)
      (bind-key "M-<backspace>" 'ignore)
      (bind-key "M-<kp-delete>" 'ignore)
      (bind-key "M-`" 'other-frame)
      (require 'cl)
      ;; Need to fix `org-meta(left|right)' as well. TODO: switch to
      ;; noflet after this is merged:
      ;; https://github.com/nicferrier/emacs-noflet/pull/17
      (define-advice org-metaleft (:around (orig-fun &rest args) osx-command)
        (flet ((backward-word (&rest args)))
          (defun backward-word (&rest args)
            (interactive)
            (call-interactively #'move-beginning-of-line))
          (apply orig-fun args)))
      (define-advice org-metaright (:around (orig-fun &rest args) osx-command)
        (flet ((forward-word (&rest args)))
          (defun forward-word (&rest args)
            (interactive)
            (call-interactively #'move-end-of-line))
          (apply orig-fun args))))))

Bind home/end to bol/eol

Mac OS binds Home and End to go to start/end of buffer by default. We want the windows-like behavior, where Home/End go to start/end of line, and Control+Home/End go to start/end of buffer.

(bind-key "<home>" 'move-beginning-of-line)
(bind-key "<end>" 'move-end-of-line)

Have indent-region indent containing defun if mark is inactive

(define-advice indent-region
    (:around (orig-fun &rest args) indent-defun)
  "Indent containing defun if mark is not active."
  (if (and transient-mark-mode
           (not mark-active))
      (save-excursion
        (mark-defun)
        (call-interactively #'indent-region))
    (apply orig-fun args)))

Always indent after newline

(bind-key "RET" #'newline-and-indent)

Turn off electric-indent-mode in markdown buffers

electric-indent-mode has a bad interaction with markdown-mode, so we disable it in markdown buffers only.

(add-hook 'markdown-mode-hook
          (apply-partially #'electric-indent-local-mode 0))

Turn on eldoc mode in elisp modes

(cl-loop
 for hook in
 '(lisp-interaction-mode-hook emacs-lisp-mode-hook)
 do (add-hook hook #'eldoc-mode))

TRAMP

Tramp remote sudo

This allows TRAMP to use sudo on remote hosts.

(require 'tramp)
(add-to-list 'tramp-default-proxies-alist
             '(nil "\\`root\\'" "/ssh:%h:"))
(add-to-list 'tramp-default-proxies-alist
             (list (regexp-quote (system-name)) nil nil))

Disable TRAMP backups entirely for su/sudo

See this page for infromation on why this is needed for security.

(defvar tramp-backup-forbidden-methods
  '("su" "sudo")
  "TRAMP methods for which backup files should be forbidden")

(defun backup-enable-predicate-custom (name)
  "See `tramp-backup-forbidden-methods'"
  (and (normal-backup-enable-predicate name)
       (not
        (let ((method (file-remote-p name 'method)))
          (when (stringp method)
            (member method tramp-backup-forbidden-methods))))))

(setq backup-enable-predicate #'backup-enable-predicate-custom)

Make TRAMP backup files on local filesystem

This ensures that backup files for TRAMP remote files are created on the local machine. This is safe because backup files for su/sudo files are disabled entirely above.

(setq tramp-backup-directory-alist nil)

Use conf-mode for .gitignore files

(add-to-list 'auto-mode-alist '("\\.gitignore\\'" . conf-mode))

Macro for suppressing messages

(defmacro without-messages (&rest body)
  "Evaluate BODY but ignore all messages.

This temporarily binds the `message' function to `ignore' while
executing BODY."
  (declare (indent 0))
  `(noflet ((message (&rest ignore) nil))
     ,@body))

Emacs desktop additions

The following additions ensure that the saved desktop file is always up-to-date.

Add a desktop-save function that gives up if user input is required

When running in hooks, it’s not disastrous if we can’t save the desktop for some reason, and we don’t want to bother the user, so we wrap the normal saving function to force it to do nothing instead of asking for user input.

(defun desktop-autosave-in-desktop-dir ()
  "Like `desktop-save-in-desktop-dir' but aborts if input is required.

If `desktop-save-in-desktop-dir' tries to solicit user input,
this aborts and returns nil instead. Also, it disables all
messages during desktop saving. This is intended for use in place
of `desktop-save-in-desktop-dir' in hooks where you don't want to
bother the user if something weird happens."
  (interactive)
  (without-user-input
    (without-messages
     (desktop-save-in-desktop-dir))))

Save desktop with every autosave

(add-hook 'auto-save-hook 'desktop-autosave-in-desktop-dir)

Save desktop after opening or closing a file

This will ensure that all open files are saved in the desktop. An idle timer and tripwire variable are used used to avoid saving the desktop multiple times when multiple files are opened or closed in rapid succession.

(defvar desktop-mode-desktop-is-stale nil
  "This is set to non-nil when a file is opened or closed.")

(defun desktop-mode-set-stale ()
  "If current buffer has a file, set the stale desktop flag."
  (when buffer-file-name
    (setq desktop-mode-desktop-is-stale t)))
(defun desktop-mode-set-current ()
  "Unconditionally clear the stale desktop flag."
  (setq desktop-mode-desktop-is-stale nil))
(add-hook 'kill-buffer-hook #'desktop-mode-set-stale)
(add-hook 'find-file-hook #'desktop-mode-set-stale)
(add-hook 'desktop-after-read-hook #'desktop-mode-set-current)

(defun desktop-mode-save-if-stale ()
  (when desktop-mode-desktop-is-stale
    (desktop-autosave-in-desktop-dir)
    (desktop-mode-set-current)))

;; Desktop will be saved 0.1 seconds after any file is opened or
;; closed.
(run-with-idle-timer 0.1 t #'desktop-mode-save-if-stale)

Auto-steal desktop if current owner is dead

The desktop-owner function should only ever return the PID of an Emacs process that’s currently running. This advice replaces the PID of a dead or non-Emacs process with nil, thus allowing the current Emacs to pry the desktop file from the cold dead hands of the previous one without asking permisssion.

(defun pid-command-line (pid)
  "Return the command line for process with the specified PID.

If PID is not a currently running process, returns nil."
  (ignore-errors
      (car (process-lines "ps" "-p" (format "%s" pid) "-o" "args="))))

(define-advice desktop-owner (:filter-return (retval) pry-from-cold-dead-hands)
  "Only return the PID of an Emacs process or nil.

If the return value is not the PID of a currently running Emacs
owned by the current user, it is replaced with nil on the
assumption that the previous owner died an untimely death, so
that the current emacs can cleanly claim its inheritence."
  (ignore-errors
    (let ((owner-cmd (pid-command-line retval)))
      (unless (and owner-cmd
                   (string-match-p
                    "emacs"
                    (downcase (file-name-base owner-cmd))))
        (setq retval nil))))
  retval)

Prevent recursive invocations of desktop-save

If desktop-save needs to ask a question and Emacs is idle for a long time (multiple auto-save intervals), it is possible to get multiple nested calls to save the desktop. This is obviously undesirable. The below code turns any recursive call to desktop-save with the same DIRNAME into a no-op.

(defvar desktop-save-recursion-guard-dirname nil)

(define-advice desktop-save (:around (orig-fun dirname &rest args) prevent-recursion)
  "Prevent recursive calls to `desktop-save'.

Recursive calls will only be prevented when they have the same
DIRNAME."
  (if (string= dirname desktop-save-recursion-guard-dirname)
      (message "Preventing recursive call to `desktop-save' for %S" dirname)
    (let ((desktop-save-recursion-guard-dirname dirname))
      (apply orig-fun dirname args))))

Put enabled/disabled commands in custom.el

By default, enable-command and disable-command append their declarations to user-init-file. But I want them appended to custom.el instead.

(define-advice en/disable-command (:around (orig-fun &rest args) put-in-custom-file)
  "Put declarations in `custom-file'."
  (let ((user-init-file custom-file))
    (apply orig-fun args)))

Fix diff behavior when backup file is not in same directory

My settings put all backup files in one directory (see backup-directory-alist). So when diff prompts for the second file, it starts in that backup directory. I would rather have it start in the same directory as the first file.

;; (define-advice diff (:before (&rest args) same-dir-for-both-files)
;;   "Only prompt with backup file in same directory.

;; When called interactively, `diff' normally offers to compare
;; against the latest backup file of the selected file. But this
;; isn't great if that backup file is in a dedicated backup
;; directory far away from the original directory. So this advice
;; only allows it to offer backup files from the same directory.

;; This advice doesn't actually modify the function's behavior in
;; any way. It simply overrides the interactive form."
;;   (interactive
;;    (let* ((newf (if (and buffer-file-name (file-exists-p buffer-file-name))
;;                     (read-file-name
;;                      (concat "Diff new file (default "
;;                              (file-name-nondirectory buffer-file-name) "): ")
;;                      nil buffer-file-name t)
;;                   (read-file-name "Diff new file: " nil nil t)))
;;           (oldf (file-newest-backup newf)))
;;      (setq oldf (if (and oldf (file-exists-p oldf)
;;                          (f-same? (f-dirname newf) (f-dirname oldf)))
;;                     (read-file-name
;;                      (concat "Diff original file (default "
;;                              (file-name-nondirectory oldf) "): ")
;;                      (file-name-directory oldf) oldf t)
;;                   (read-file-name "Diff original file: "
;;                                   (file-name-directory newf) nil t)))
;;      (list oldf newf (diff-switches)))))

Test the fix for this

(ignore-errors (load "~/.emacs.d/ido.el"))

Get this merged into Emacs

This is actually an ido bug: see:

Fix value of x-colors

For some reason the x-colors variable has started to get the wrong value, so I’ve copied the code to set it correctly out of common-win.el.

(setq x-colors
  (if (featurep 'ns) (funcall #'ns-list-colors)
    (purecopy
     '("gray100" "grey100" "gray99" "grey99" "gray98" "grey98" "gray97"
       "grey97" "gray96" "grey96" "gray95" "grey95" "gray94" "grey94"
       "gray93" "grey93" "gray92" "grey92" "gray91" "grey91" "gray90"
       "grey90" "gray89" "grey89" "gray88" "grey88" "gray87" "grey87"
       "gray86" "grey86" "gray85" "grey85" "gray84" "grey84" "gray83"
       "grey83" "gray82" "grey82" "gray81" "grey81" "gray80" "grey80"
       "gray79" "grey79" "gray78" "grey78" "gray77" "grey77" "gray76"
       "grey76" "gray75" "grey75" "gray74" "grey74" "gray73" "grey73"
       "gray72" "grey72" "gray71" "grey71" "gray70" "grey70" "gray69"
       "grey69" "gray68" "grey68" "gray67" "grey67" "gray66" "grey66"
       "gray65" "grey65" "gray64" "grey64" "gray63" "grey63" "gray62"
       "grey62" "gray61" "grey61" "gray60" "grey60" "gray59" "grey59"
       "gray58" "grey58" "gray57" "grey57" "gray56" "grey56" "gray55"
       "grey55" "gray54" "grey54" "gray53" "grey53" "gray52" "grey52"
       "gray51" "grey51" "gray50" "grey50" "gray49" "grey49" "gray48"
       "grey48" "gray47" "grey47" "gray46" "grey46" "gray45" "grey45"
       "gray44" "grey44" "gray43" "grey43" "gray42" "grey42" "gray41"
       "grey41" "gray40" "grey40" "gray39" "grey39" "gray38" "grey38"
       "gray37" "grey37" "gray36" "grey36" "gray35" "grey35" "gray34"
       "grey34" "gray33" "grey33" "gray32" "grey32" "gray31" "grey31"
       "gray30" "grey30" "gray29" "grey29" "gray28" "grey28" "gray27"
       "grey27" "gray26" "grey26" "gray25" "grey25" "gray24" "grey24"
       "gray23" "grey23" "gray22" "grey22" "gray21" "grey21" "gray20"
       "grey20" "gray19" "grey19" "gray18" "grey18" "gray17" "grey17"
       "gray16" "grey16" "gray15" "grey15" "gray14" "grey14" "gray13"
       "grey13" "gray12" "grey12" "gray11" "grey11" "gray10" "grey10"
       "gray9" "grey9" "gray8" "grey8" "gray7" "grey7" "gray6" "grey6"
       "gray5" "grey5" "gray4" "grey4" "gray3" "grey3" "gray2" "grey2"
       "gray1" "grey1" "gray0" "grey0"
       "LightPink1" "LightPink2" "LightPink3" "LightPink4"
       "pink1" "pink2" "pink3" "pink4"
       "PaleVioletRed1" "PaleVioletRed2" "PaleVioletRed3" "PaleVioletRed4"
       "LavenderBlush1" "LavenderBlush2" "LavenderBlush3" "LavenderBlush4"
       "VioletRed1" "VioletRed2" "VioletRed3" "VioletRed4"
       "HotPink1" "HotPink2" "HotPink3" "HotPink4"
       "DeepPink1" "DeepPink2" "DeepPink3" "DeepPink4"
       "maroon1" "maroon2" "maroon3" "maroon4"
       "orchid1" "orchid2" "orchid3" "orchid4"
       "plum1" "plum2" "plum3" "plum4"
       "thistle1" "thistle2" "thistle3" "thistle4"
       "MediumOrchid1" "MediumOrchid2" "MediumOrchid3" "MediumOrchid4"
       "DarkOrchid1" "DarkOrchid2" "DarkOrchid3" "DarkOrchid4"
       "purple1" "purple2" "purple3" "purple4"
       "MediumPurple1" "MediumPurple2" "MediumPurple3" "MediumPurple4"
       "SlateBlue1" "SlateBlue2" "SlateBlue3" "SlateBlue4"
       "RoyalBlue1" "RoyalBlue2" "RoyalBlue3" "RoyalBlue4"
       "LightSteelBlue1" "LightSteelBlue2" "LightSteelBlue3" "LightSteelBlue4"
       "SlateGray1" "SlateGray2" "SlateGray3" "SlateGray4"
       "DodgerBlue1" "DodgerBlue2" "DodgerBlue3" "DodgerBlue4"
       "SteelBlue1" "SteelBlue2" "SteelBlue3" "SteelBlue4"
       "SkyBlue1" "SkyBlue2" "SkyBlue3" "SkyBlue4"
       "LightSkyBlue1" "LightSkyBlue2" "LightSkyBlue3" "LightSkyBlue4"
       "LightBlue1" "LightBlue2" "LightBlue3" "LightBlue4"
       "CadetBlue1" "CadetBlue2" "CadetBlue3" "CadetBlue4"
       "azure1" "azure2" "azure3" "azure4"
       "LightCyan1" "LightCyan2" "LightCyan3" "LightCyan4"
       "PaleTurquoise1" "PaleTurquoise2" "PaleTurquoise3" "PaleTurquoise4"
       "DarkSlateGray1" "DarkSlateGray2" "DarkSlateGray3" "DarkSlateGray4"
       "aquamarine1" "aquamarine2" "aquamarine3" "aquamarine4"
       "SeaGreen1" "SeaGreen2" "SeaGreen3" "SeaGreen4"
       "honeydew1" "honeydew2" "honeydew3" "honeydew4"
       "DarkSeaGreen1" "DarkSeaGreen2" "DarkSeaGreen3" "DarkSeaGreen4"
       "PaleGreen1" "PaleGreen2" "PaleGreen3" "PaleGreen4"
       "DarkOliveGreen1" "DarkOliveGreen2" "DarkOliveGreen3" "DarkOliveGreen4"
       "OliveDrab1" "OliveDrab2" "OliveDrab3" "OliveDrab4"
       "ivory1" "ivory2" "ivory3" "ivory4"
       "LightYellow1" "LightYellow2" "LightYellow3" "LightYellow4"
       "khaki1" "khaki2" "khaki3" "khaki4"
       "LemonChiffon1" "LemonChiffon2" "LemonChiffon3" "LemonChiffon4"
       "LightGoldenrod1" "LightGoldenrod2" "LightGoldenrod3" "LightGoldenrod4"
       "cornsilk1" "cornsilk2" "cornsilk3" "cornsilk4"
       "goldenrod1" "goldenrod2" "goldenrod3" "goldenrod4"
       "DarkGoldenrod1" "DarkGoldenrod2" "DarkGoldenrod3" "DarkGoldenrod4"
       "wheat1" "wheat2" "wheat3" "wheat4"
       "NavajoWhite1" "NavajoWhite2" "NavajoWhite3" "NavajoWhite4"
       "burlywood1" "burlywood2" "burlywood3" "burlywood4"
       "AntiqueWhite1" "AntiqueWhite2" "AntiqueWhite3" "AntiqueWhite4"
       "bisque1" "bisque2" "bisque3" "bisque4"
       "tan1" "tan2" "tan3" "tan4"
       "PeachPuff1" "PeachPuff2" "PeachPuff3" "PeachPuff4"
       "seashell1" "seashell2" "seashell3" "seashell4"
       "chocolate1" "chocolate2" "chocolate3" "chocolate4"
       "sienna1" "sienna2" "sienna3" "sienna4"
       "LightSalmon1" "LightSalmon2" "LightSalmon3" "LightSalmon4"
       "salmon1" "salmon2" "salmon3" "salmon4"
       "coral1" "coral2" "coral3" "coral4"
       "tomato1" "tomato2" "tomato3" "tomato4"
       "MistyRose1" "MistyRose2" "MistyRose3" "MistyRose4"
       "snow1" "snow2" "snow3" "snow4"
       "RosyBrown1" "RosyBrown2" "RosyBrown3" "RosyBrown4"
       "IndianRed1" "IndianRed2" "IndianRed3" "IndianRed4"
       "firebrick1" "firebrick2" "firebrick3" "firebrick4"
       "brown1" "brown2" "brown3" "brown4"
       "magenta1" "magenta2" "magenta3" "magenta4"
       "blue1" "blue2" "blue3" "blue4"
       "DeepSkyBlue1" "DeepSkyBlue2" "DeepSkyBlue3" "DeepSkyBlue4"
       "turquoise1" "turquoise2" "turquoise3" "turquoise4"
       "cyan1" "cyan2" "cyan3" "cyan4"
       "SpringGreen1" "SpringGreen2" "SpringGreen3" "SpringGreen4"
       "green1" "green2" "green3" "green4"
       "chartreuse1" "chartreuse2" "chartreuse3" "chartreuse4"
       "yellow1" "yellow2" "yellow3" "yellow4"
       "gold1" "gold2" "gold3" "gold4"
       "orange1" "orange2" "orange3" "orange4"
       "DarkOrange1" "DarkOrange2" "DarkOrange3" "DarkOrange4"
       "OrangeRed1" "OrangeRed2" "OrangeRed3" "OrangeRed4"
       "red1" "red2" "red3" "red4"
       "lavender blush" "LavenderBlush" "ghost white" "GhostWhite"
       "lavender" "alice blue" "AliceBlue" "azure" "light cyan"
       "LightCyan" "mint cream" "MintCream" "honeydew" "ivory"
       "light goldenrod yellow" "LightGoldenrodYellow" "light yellow"
       "LightYellow" "beige" "floral white" "FloralWhite" "old lace"
       "OldLace" "blanched almond" "BlanchedAlmond" "moccasin"
       "papaya whip" "PapayaWhip" "bisque" "antique white"
       "AntiqueWhite" "linen" "peach puff" "PeachPuff" "seashell"
       "misty rose" "MistyRose" "snow" "light pink" "LightPink" "pink"
       "hot pink" "HotPink" "deep pink" "DeepPink" "maroon"
       "pale violet red" "PaleVioletRed" "violet red" "VioletRed"
       "medium violet red" "MediumVioletRed" "violet" "plum" "thistle"
       "orchid" "medium orchid" "MediumOrchid" "dark orchid"
       "DarkOrchid" "purple" "blue violet" "BlueViolet" "medium purple"
       "MediumPurple" "light slate blue" "LightSlateBlue"
       "medium slate blue" "MediumSlateBlue" "slate blue" "SlateBlue"
       "dark slate blue" "DarkSlateBlue" "midnight blue" "MidnightBlue"
       "navy" "navy blue" "NavyBlue" "dark blue" "DarkBlue"
       "light steel blue" "LightSteelBlue" "cornflower blue"
       "CornflowerBlue" "dodger blue" "DodgerBlue" "royal blue"
       "RoyalBlue" "light slate gray" "light slate grey"
       "LightSlateGray" "LightSlateGrey" "slate gray" "slate grey"
       "SlateGray" "SlateGrey" "dark slate gray" "dark slate grey"
       "DarkSlateGray" "DarkSlateGrey" "steel blue" "SteelBlue"
       "cadet blue" "CadetBlue" "light sky blue" "LightSkyBlue"
       "sky blue" "SkyBlue" "light blue" "LightBlue" "powder blue"
       "PowderBlue" "pale turquoise" "PaleTurquoise" "turquoise"
       "medium turquoise" "MediumTurquoise" "dark turquoise"
       "DarkTurquoise"  "dark cyan" "DarkCyan" "aquamarine"
       "medium aquamarine" "MediumAquamarine" "light sea green"
       "LightSeaGreen" "medium sea green" "MediumSeaGreen" "sea green"
       "SeaGreen" "dark sea green" "DarkSeaGreen" "pale green"
       "PaleGreen" "lime green" "LimeGreen" "dark green" "DarkGreen"
       "forest green" "ForestGreen" "light green" "LightGreen"
       "green yellow" "GreenYellow" "yellow green" "YellowGreen"
       "olive drab" "OliveDrab" "dark olive green" "DarkOliveGreen"
       "lemon chiffon" "LemonChiffon" "khaki" "dark khaki" "DarkKhaki"
       "cornsilk" "pale goldenrod" "PaleGoldenrod" "light goldenrod"
       "LightGoldenrod" "goldenrod" "dark goldenrod" "DarkGoldenrod"
       "wheat" "navajo white" "NavajoWhite" "tan" "burlywood"
       "sandy brown" "SandyBrown" "peru" "chocolate" "saddle brown"
       "SaddleBrown" "sienna" "rosy brown" "RosyBrown" "dark salmon"
       "DarkSalmon" "coral" "tomato" "light salmon" "LightSalmon"
       "salmon" "light coral" "LightCoral" "indian red" "IndianRed"
       "firebrick" "brown" "dark red" "DarkRed" "magenta"
       "dark magenta" "DarkMagenta" "dark violet" "DarkViolet"
       "medium blue" "MediumBlue" "blue" "deep sky blue" "DeepSkyBlue"
       "cyan" "medium spring green" "MediumSpringGreen" "spring green"
       "SpringGreen" "green" "lawn green" "LawnGreen" "chartreuse"
       "yellow" "gold" "orange" "dark orange" "DarkOrange" "orange red"
       "OrangeRed" "red" "white" "white smoke" "WhiteSmoke" "gainsboro"
       "light gray" "light grey" "LightGray" "LightGrey" "gray" "grey"
       "dark gray" "dark grey" "DarkGray" "DarkGrey" "dim gray"
       "dim grey" "DimGray" "DimGrey" "black"))))

Associate “*.latex” with latex-mode

By default “.ltx” is assoiated with LaTeX files, but not “.latex”.

(add-to-list 'auto-mode-alist '("\\.latex\\'" . latex-mode))

Use conf-mode for git config files

(add-to-list 'auto-mode-alist
             '("\\.gitconfig\\'" . conf-mode))
(add-to-list 'auto-mode-alist
             (cons (concat (regexp-quote (f-join ".git" "config")) "\\'")
                   'conf-mode))

Fix report-emacs-bug-insert-to-mailer

For some unknown reason, on my system xdg-email does nothing (but still exits successfully) when started through start-process. So we use call-process instead.

(define-advice report-emacs-bug-insert-to-mailer
    (:around (orig-fun &rest args) use-call-process)
  "Use `call-process' instead of `start-process'.

For some reason \"xdg-email\" doesn't work from `start-process',
so we use `call-process' instead. This is fine because both the
OS X \"open\" and unix \"xdg-email\" commands exit
immediately."
  (noflet ((start-process (name buffer program &rest program-args)
                          (apply #'call-process program nil buffer nil program-args)))
    (apply orig-fun args)))

Define functions for initiating external mailer composition

Function to send en email to external mailer

(defun insert-to-mailer (&optional arg-ignored)
  "Send the message to your preferred mail client.
This requires either the macOS \"open\" command, or the freedesktop
\"xdg-email\" command to be available.

This function accepts a prefix argument for consistency with
`message-send', but the prefix argument has no effect."
  (interactive)
  (save-excursion
    ;; FIXME? use mail-fetch-field?
    (let* ((to (progn
                 (goto-char (point-min))
                 (forward-line)
                 (and (looking-at "^To: \\(.*\\)")
                      (match-string-no-properties 1))))
           (subject (progn
                      (forward-line)
                      (and (looking-at "^Subject: \\(.*\\)")
                           (match-string-no-properties 1))))
           (body (progn
                   (forward-line 2)
                   (buffer-substring-no-properties (point) (point-max)))))
      (if (and to subject body)
          (if (report-emacs-bug-can-use-osx-open)
              (start-process "/usr/bin/open" nil "open"
                             (concat "mailto:" to
                                     "?subject=" (url-hexify-string subject)
                                     "&body=" (url-hexify-string body)))
            (start-process "xdg-email" nil "xdg-email"
                           "--subject" subject
                           "--body" body
                           (concat "mailto:" to)))
        (error "Subject, To or body not found")))))

(defun insert-to-mailer-and-exit (&optional arg)
  "Send message like `insert-to-mailer', then, if no errors, exit from mail buffer.

This function accepts a prefix argument for consistency with
`message-send-and-exit', but the prefix argument has no effect."
  (interactive "P")
  (let ((buf (current-buffer))
        (actions message-exit-actions))
    (when (and (insert-to-mailer arg)
               (buffer-name buf))
      (message-bury buf)
      (if message-kill-buffer-on-exit
          (kill-buffer buf))
      (message-do-actions actions)
      t)))

Define mail-user-agent for external mailer

(define-mail-user-agent 'external-mailer-user-agent
  (get 'message-user-agent 'composefunc)
  #'insert-to-mailer-and-exit
  (get 'message-user-agent 'abortfunc)
  (get 'message-user-agent 'hookvar))

Eliminate trailing semicolon in propline variable list

Emacs functions that modify the local variables in the propline also add an extraneous trailing semicolon. This advice deletes it.

(define-advice modify-file-local-variable-prop-line
    (:around (orig-fun &rest args) cleanup-semicolon)
  "Delete the trailing semicolon."
  (atomic-change-group
    (apply orig-fun args)
    (save-excursion
      (goto-char (point-min))
      (let ((replace-lax-whitespace t))
        (replace-string "; -*-" " -*-" nil
                        (point) (progn (end-of-line) (point)))))))

Associate .zsh files with zshell in sh-mode

Emacs sh-mode doesn’t automatically associate *.zsh with zsh. This enables that. It also enables it for a few other zsh-related files.

;; Files ending in .zsh
(add-to-list 'auto-mode-alist '("\\.zsh\\'" . sh-mode))
;; zsh startup files
(add-to-list 'auto-mode-alist '("\\.\\(zshrc\\|zshenv\\|zprofile\\|zlogin\\|zlogout\\)\\>" . sh-mode))
;; Ensure that sh-mode uses zsh as shell for these files
(defun sh-mode-set-zsh-by-file-name ()
  (when (and buffer-file-name
             (string-match-p "\\.zsh\\(rc\\|env\\|\\'\\)" buffer-file-name))
    (sh-set-shell "zsh")))
(add-hook 'sh-mode-hook 'sh-mode-set-zsh-by-file-name)

Add sort-words command

Emacs has a command to sort lines, but not to sort words in a region.

(defun sort-words (reverse beg end)
  "Sort words in region alphabetically, in REVERSE if negative.
Prefixed with negative \\[universal-argument], sorts in reverse.

The variable `sort-fold-case' determines whether alphabetic case
affects the sort order.

See `sort-regexp-fields'."
  (interactive "*P\nr")
  (sort-regexp-fields reverse "\\w+" "\\&" beg end))

Only enable git-gutter in local files

Git-gutter doesn’t play nice with TRAMP remotes

(defun git-gutter-find-file-hook ()
  (git-gutter-mode
   (if (file-remote-p (buffer-file-name))
       0
     1)))
(add-hook 'find-file-hook #'git-gutter-find-file-hook)

Make scripts executable on save

If a file begins with a shebang (i.e. “#!”), make it executable after saving it.

(add-hook 'after-save-hook
  'executable-make-buffer-file-executable-if-script-p)

Make electric-indent-mode and python-mode play nice

(defun python-newline-and-indent ()
  "Custom python indentation function.

  This works like normal, except that if point is in the
  indentation of the current line, the newly created line will
  not be indented any further than the current line. This fixes
  the annoying tendency of python-mode to always indent to the
  maximum possible indentation level on every new line."
  (interactive)
  (let* ((starting-column (current-column))
         (starting-indentation (current-indentation))
         (started-in-indentation (<= starting-column starting-indentation)))
    (newline-and-indent)
    (when (and started-in-indentation
               (> (current-indentation) starting-indentation))
      (save-excursion
        (back-to-indentation)
        (delete-region (point) (progn (forward-line 0) (point)))
        (indent-to-column starting-indentation))
      (back-to-indentation))))
(define-key python-mode-map (kbd "RET") #'python-newline-and-indent)
(defun turn-off-electric-indent-local-mode ()
    (electric-indent-local-mode 0))
(add-hook 'python-mode-hook #'turn-off-electric-indent-local-mode)

ESS default directory fix

When an R script is in a directory named “scripts”, suggest the parent directory as the starting directory.

(require 'f)
(defun my-ess-directory-function ()
  (cond (ess-directory)
        ((string= "scripts" (f-filename (f-full default-directory)))
         (f-parent default-directory))
        (t nil)))
(setq ess-directory-function #'my-ess-directory-function)

Call req-package-finish when evaluating req-package forms interactively

When I use “C-x C-e” on a req-package declaration, I usually want the package to be installed immediately without having to call req-package-finish manually. This advice does that.

(define-advice eval-last-sexp (:around (orig-fun &rest args) req-package-eagerly)
  "Call `req-package-finish' afterward if evaluating a `req-package' form."
  (let ((is-req-package
         (eq (car-safe (elisp--preceding-sexp))
             'req-package)))
    (prog1 (apply orig-fun args)
      (ignore-errors
        (when is-req-package
          (req-package-finish))))))

Enable Fira Code font ligatures

These are disabled for now due to the error documented here:

Update: These are now disabled because they have started messing up the spacing of things, which is weird because they’re supposed to take up the same space as the normal characters they replace.

;; ;;; Fira code
;; ;; This works when using emacs --daemon + emacsclient
;; (add-hook 'after-make-frame-functions (lambda (frame) (set-fontset-font t '(#Xe100 . #Xe16f) "Fira Code Symbol")))
;; ;; This works when using emacs without server/client
;; (set-fontset-font t '(#Xe100 . #Xe16f) "Fira Code Symbol")
;; ;; I haven't found one statement that makes both of the above situations work, so I use both for now

;; (defconst fira-code-font-lock-keywords-alist
;;   (mapcar (lambda (regex-char-pair)
;;             `(,(car regex-char-pair)
;;               (0 (prog1 ()
;;                    (compose-region (match-beginning 1)
;;                                    (match-end 1)
;;                                    ;; The first argument to concat is a string containing a literal tab
;;                                    ,(concat "	" (list (decode-char 'ucs (cadr regex-char-pair)))))))))
;;           '(("\\(www\\)"                   #Xe100)
;;             ("[^/]\\(\\*\\*\\)[^/]"        #Xe101)
;;             ("\\(\\*\\*\\*\\)"             #Xe102)
;;             ("\\(\\*\\*/\\)"               #Xe103)
;;             ("\\(\\*>\\)"                  #Xe104)
;;             ("[^*]\\(\\*/\\)"              #Xe105)
;;             ("\\(\\\\\\\\\\)"              #Xe106)
;;             ("\\(\\\\\\\\\\\\\\)"          #Xe107)
;;             ("\\({-\\)"                    #Xe108)
;;             ("\\(\\[\\]\\)"                #Xe109)
;;             ("\\(::\\)"                    #Xe10a)
;;             ("\\(:::\\)"                   #Xe10b)
;;             ("[^=]\\(:=\\)"                #Xe10c)
;;             ("\\(!!\\)"                    #Xe10d)
;;             ("\\(!=\\)"                    #Xe10e)
;;             ("\\(!==\\)"                   #Xe10f)
;;             ("\\(-}\\)"                    #Xe110)
;;             ("\\(--\\)"                    #Xe111)
;;             ("\\(---\\)"                   #Xe112)
;;             ("\\(-->\\)"                   #Xe113)
;;             ("[^-]\\(->\\)"                #Xe114)
;;             ("\\(->>\\)"                   #Xe115)
;;             ("\\(-<\\)"                    #Xe116)
;;             ("\\(-<<\\)"                   #Xe117)
;;             ("\\(-~\\)"                    #Xe118)
;;             ("\\(#{\\)"                    #Xe119)
;;             ("\\(#\\[\\)"                  #Xe11a)
;;             ("\\(##\\)"                    #Xe11b)
;;             ("\\(###\\)"                   #Xe11c)
;;             ("\\(####\\)"                  #Xe11d)
;;             ("\\(#(\\)"                    #Xe11e)
;;             ("\\(#\\?\\)"                  #Xe11f)
;;             ("\\(#_\\)"                    #Xe120)
;;             ("\\(#_(\\)"                   #Xe121)
;;             ("\\(\\.-\\)"                  #Xe122)
;;             ("\\(\\.=\\)"                  #Xe123)
;;             ("\\(\\.\\.\\)"                #Xe124)
;;             ("\\(\\.\\.<\\)"               #Xe125)
;;             ("\\(\\.\\.\\.\\)"             #Xe126)
;;             ("\\(\\?=\\)"                  #Xe127)
;;             ("\\(\\?\\?\\)"                #Xe128)
;;             ("\\(;;\\)"                    #Xe129)
;;             ("\\(/\\*\\)"                  #Xe12a)
;;             ("\\(/\\*\\*\\)"               #Xe12b)
;;             ("\\(/=\\)"                    #Xe12c)
;;             ("\\(/==\\)"                   #Xe12d)
;;             ("\\(/>\\)"                    #Xe12e)
;;             ("\\(//\\)"                    #Xe12f)
;;             ("\\(///\\)"                   #Xe130)
;;             ("\\(&&\\)"                    #Xe131)
;;             ("\\(||\\)"                    #Xe132)
;;             ("\\(||=\\)"                   #Xe133)
;;             ("[^|]\\(|=\\)"                #Xe134)
;;             ("\\(|>\\)"                    #Xe135)
;;             ("\\(\\^=\\)"                  #Xe136)
;;             ("\\(\\$>\\)"                  #Xe137)
;;             ("\\(\\+\\+\\)"                #Xe138)
;;             ("\\(\\+\\+\\+\\)"             #Xe139)
;;             ("\\(\\+>\\)"                  #Xe13a)
;;             ("\\(=:=\\)"                   #Xe13b)
;;             ("[^!/]\\(==\\)[^>]"           #Xe13c)
;;             ("\\(===\\)"                   #Xe13d)
;;             ("\\(==>\\)"                   #Xe13e)
;;             ("[^=]\\(=>\\)"                #Xe13f)
;;             ("\\(=>>\\)"                   #Xe140)
;;             ("\\(<=\\)"                    #Xe141)
;;             ("\\(=<<\\)"                   #Xe142)
;;             ("\\(=/=\\)"                   #Xe143)
;;             ("\\(>-\\)"                    #Xe144)
;;             ("\\(>=\\)"                    #Xe145)
;;             ("\\(>=>\\)"                   #Xe146)
;;             ("[^-=]\\(>>\\)"               #Xe147)
;;             ("\\(>>-\\)"                   #Xe148)
;;             ("\\(>>=\\)"                   #Xe149)
;;             ("\\(>>>\\)"                   #Xe14a)
;;             ("\\(<\\*\\)"                  #Xe14b)
;;             ("\\(<\\*>\\)"                 #Xe14c)
;;             ("\\(<|\\)"                    #Xe14d)
;;             ("\\(<|>\\)"                   #Xe14e)
;;             ("\\(<\\$\\)"                  #Xe14f)
;;             ("\\(<\\$>\\)"                 #Xe150)
;;             ("\\(<!--\\)"                  #Xe151)
;;             ("\\(<-\\)"                    #Xe152)
;;             ("\\(<--\\)"                   #Xe153)
;;             ("\\(<->\\)"                   #Xe154)
;;             ("\\(<\\+\\)"                  #Xe155)
;;             ("\\(<\\+>\\)"                 #Xe156)
;;             ("\\(<=\\)"                    #Xe157)
;;             ("\\(<==\\)"                   #Xe158)
;;             ("\\(<=>\\)"                   #Xe159)
;;             ("\\(<=<\\)"                   #Xe15a)
;;             ("\\(<>\\)"                    #Xe15b)
;;             ("[^-=]\\(<<\\)"               #Xe15c)
;;             ("\\(<<-\\)"                   #Xe15d)
;;             ("\\(<<=\\)"                   #Xe15e)
;;             ("\\(<<<\\)"                   #Xe15f)
;;             ("\\(<~\\)"                    #Xe160)
;;             ("\\(<~~\\)"                   #Xe161)
;;             ("\\(</\\)"                    #Xe162)
;;             ("\\(</>\\)"                   #Xe163)
;;             ("\\(~@\\)"                    #Xe164)
;;             ("\\(~-\\)"                    #Xe165)
;;             ("\\(~=\\)"                    #Xe166)
;;             ("\\(~>\\)"                    #Xe167)
;;             ("[^<]\\(~~\\)"                #Xe168)
;;             ("\\(~~>\\)"                   #Xe169)
;;             ("\\(%%\\)"                    #Xe16a)
;;            ;; ("\\(x\\)"                   #Xe16b) This ended up being hard to do properly so i'm leaving it out.
;;             ("[^:=]\\(:\\)[^:=]"           #Xe16c)
;;             ("[^\\+<>]\\(\\+\\)[^\\+<>]"   #Xe16d)
;;             ("[^\\*/<>]\\(\\*\\)[^\\*/<>]" #Xe16f))))

;; (defun add-fira-code-symbol-keywords ()
;;   (font-lock-add-keywords nil fira-code-font-lock-keywords-alist))

;; (add-hook 'prog-mode-hook
;;           #'add-fira-code-symbol-keywords)

Ignore left/right scroll events

If wheel-left and wheel-right are left unbound, Emacs rings the bell when they are used, which can easily happen when scrolling on a touchpad. To prevent the bell from ringing, we add no-op bindings to these events.

(bind-key "<wheel-left>" 'ignore)
(bind-key "<wheel-right>" 'ignore)

Disable suspend-frame for GUI frames

Calling suspend-frame on a GUI frame causes it to remain frozen when unminimized. So we prevent suspend-frame from working on GUI frames.

(define-advice suspend-frame (:around (orig-fun &rest args) disable-on-gui-frames)
  "Disable `suspend-frame' on GUI frames."
  (if (display-graphic-p (selected-frame))
      (message "`suspend-frame' disabled for GUI frames.")
    (apply orig-fun args)))

Make ido file sorting respect ido-case-fold

(define-advice ido-file-lessp (:around (orig-fun &rest args) respect-ido-case-fold)
  "Make sure that Ido file sorting respects `ido-case-fold'."
  (if ido-case-fold
      (let ((args-lower (mapcar #'downcase args)))
        ;; Compare case-insensitively, unless the file names differ
        ;; only in case.
        (if (apply #'string= args-lower)
            (apply orig-fun args)
          (apply orig-fun args-lower)))
    (apply orig-fun args)))

(define-advice ido-file-extension-lessp (:around (orig-fun &rest args) respect-ido-case-fold)
  "Make sure that Ido file sorting respects `ido-case-fold'."
  (if ido-case-fold
      (let ((args-lower (mapcar #'downcase args)))
        ;; Compare case-insensitively, unless the file names differ
        ;; only in case.
        (if (apply #'string= args-lower)
            (apply orig-fun args)
          (apply orig-fun args-lower)))
    (apply orig-fun args)))

Prevent face-at-point from always selecting hl-line

hl-line-mode means that the last-applied face at point will always be hl-line, which isn’t very useful. This adds advice to transiently un-highlight the current line before running face-at-point, ensuring that it returns whatever it would have returned if hl-line-mode was disabled. There is no need to re-highlight the line, as the line highlght is always re-applied after each command anyway.

(require 'hl-line)
(define-advice face-at-point (:before (&rest _ignored) avoid-hl-line)
  (ignore-errors
    (when hl-line-mode
      (hl-line-unhighlight)))
  (ignore-errors
    (when global-hl-line-mode
      (global-hl-line-unhighlight))))

Replace just-one-space with cycle-spacing

The cycle-spacing command encompasses the functionality of just-one-space and several other commands, so we remap just-one-space (M-SPC by default) to it.

(define-key global-map [remap just-one-space] 'cycle-spacing)

Replace upcase-word with upcase-dwim, etc.

The DWIM versions are equivalent to the -word versions except that if the region is active, they operate on the entire region instead.

(define-key global-map [remap upcase-word] #'upcase-dwim)
(define-key global-map [remap downcase-word] #'downcase-dwim)
(define-key global-map [remap capitlize-word] #'capitlize-dwim)

Note that this frees up C-x C-u and C-x C-l, the bindings for up/downcase-region respectively.

Define a twiddle-mode function

(defun twiddle-mode (mode)
  "If MODE is activated, then deactivate it and then activate it again.
If MODE is not active, do nothing."
  (when (eval mode)
    (funcall mode 0)
    (funcall mode 1)))

Don’t allow diff to refine large hunks

Diff hunk refinement is really slow for large hunks, so we establish a word threshold above which hunk refinement will not occur.

(defcustom diff-refine-max-words nil
  "If non-nil, disable refinement for large hunks.

If non-nil, this variable should be set to an integer, which is the
limit for the number of words in a hunk to be refined. Hunks
containing more than this number of words will not be refined.
Note that the word count includes the entire hunk: additions,
deletions, and context (and header?)."
  :group 'diff-mode
  :type '(choice
          (const :tag "No limit" nil)
          (integer
           :tag "Word limit" :value 1000
           :validate
           (lambda (widget)
             (let ((v (widget-value widget)))
               (if (and (integerp v)
                        (> v 0))
                   nil
                 (widget-put widget :error "This field should contain a positive integer")
                 widget))))))

(define-advice diff--refine-hunk (:before-while (start end &rest _ignored) diff-refine-max-words)
  "Implement logic for `diff-refine-max-words'."
  ;; With `before-while', we need to return non-nil to indicate that
  ;; hunk refinement should be allowed to occur.
  (condition-case nil
      (or
       (null diff-refine-max-words)
       (let ((hunk-word-count (count-words start end)))
         (<= hunk-word-count diff-refine-max-words)))
    ;; In case of error, do whatever would have happened if this
    ;; advice didn't exist.
    (error t)))

Make sure the auto-save folder exists

(make-directory
 (file-name-directory
  (with-temp-buffer
    (setq buffer-file-name (expand-file-name "~/temp.txt"))
    (make-auto-save-file-name)))
 t)

Automatically scroll to the left after filling

Sometimes after filling a long line with C-q, auto-fill-mode, etc., the window is still scrolled away from column 0, which isn’t useful any more. So we define advice to automatically scroll back to column 0 after any filling operation (unless doing so would put point outside the window).

(defun scroll-to-column0-after-fill (&rest _ignored)
  "Scroll current window to column 0 if possible.

If scrolling the window to column 0 would pout point outside the
window, no scrolling is done.

This function is intended to be called after calling a filling
function such as `fill-paragraph'."
  (when (and (> (window-hscroll))
             (<= (current-column) (window-width)))
    (set-window-hscroll (get-buffer-window (current-buffer)) 0)))

(cl-loop
 for fun in '(fill-paragraph do-auto-fill org-fill-paragraph)
 do (advice-add fun
                :after #'scroll-to-column0-after-fill
                '(:name "After filling, scroll back to column 0 if possible")))

Define a function for clearing a buffer from kill-ring

(defun clean-kill-ring (&optional buffer)
  "Remove entries matching BUFFER from `kill-ring'.

Also clears PRIMARY and SECONDARY selections by setting them to
the empty string and clears CLIPBOARD by setting it to the first
remaining element of `kill-ring', which should clear the
clipboard for other applications as well."
  (interactive)
  ;; Just do this so current kill definitely doesn't contain any
  ;; sensitive info. This element will be removed anyway since the
  ;; empty string matches anything.
  (kill-new "")
  (setq kill-ring
        (cl-loop
         with bufstring = (with-current-buffer (or buffer (current-buffer)) (buffer-string))
         for kill in kill-ring
         unless (string-match-p (regexp-quote kill) bufstring)
         collect kill))
  (gui-set-selection 'CLIPBOARD (or (car kill-ring) ""))
  (gui-set-selection 'PRIMARY "")
  (gui-set-selection 'SECONDARY "")
  (message "Kill ring cleared of entries matching buffer %S" (buffer-name buffer)))

Fix bad behavior in org-ctags

See https://emacs.stackexchange.com/a/76352/2046 and https://lists.gnu.org/archive/html/emacs-orgmode/2023-03/msg00299.html.

;;; work-around  for org-ctags obnoxious behavior
(with-eval-after-load 'org-ctags (setq org-open-link-functions nil))

Define a “maybe” link protocol for org-mode

(with-eval-after-load 'ol
  (org-link-set-parameters
   "maybe"
   :follow
   (lambda (path prefix)
     (condition-case err
         (org-link-open-from-string (format "[[%s]]" path))
       (error (message "Failed to open maybe link %S" path))))
   ;; This must be a lambda so it is self-contained
   :export
   (lambda (path desc backend &optional info)
     (when (symbolp backend)
       (setq backend (org-export-get-backend backend)))
     ;; Generate the non-maybe version of the link, and call the
     ;; backend's appropriate transcoder on it, but catch any error
     ;; thrown and just replace the link with its text instead.
     (let* ((real-link
             (with-temp-buffer
               (save-excursion (insert "[[" path "][" (or desc path) "]]"))
               (org-element-link-parser)))
            (real-link-transcoder (cdr (assoc 'link (org-export-get-all-transcoders backend)))))
       (condition-case err
           (funcall real-link-transcoder real-link desc info)
         (error
          (message "Skipping error during maybe link transcoding: %S" err)
          (or desc path)))))))

Fix the major mode in the scratch buffer

For some reason, Emacs sometimes starts up with the scratch buffer in Fundamental mode with no initial message. Until the root cause is isolated, we just fix this manually afterward

(eval-after-init
 '(progn
    (with-current-buffer (get-scratch-buffer-create)
      (normal-mode)
      (when (and initial-scratch-message
                 (= (point-min) (point-max)))
        (insert (substitute-command-keys initial-scratch-message))
        (goto-char (point-max))
        (set-buffer-modified-p nil)))))

Environment-specific settings

This section uses the macros defined in system-specific-settings to set options that should vary depending on which system Emacs is running on.

Set up tool-bars

Normally we want the scroll bar and menu bar disabled for maximum text space. But in Mac OS, disabling them causes various things to break, so we want to enabled them there.

(require 'frame)
(add-hook
 'after-make-frame-functions
 (lambda (frame)
   (with-selected-frame frame
     (let ((mode-arg (if-system-type-match 'darwin 1 -1)))
       (menu-bar-mode mode-arg)
       (when (fboundp #'scroll-bar-mode)
         (scroll-bar-mode mode-arg))))))

Use system trash bin

(when-system-type-match
    (list 'darwin
          '(regexp . "linux"))
  (defvar trash-command "trash")

  (defun system-move-file-to-trash (filename)
    "Move file to OS X/linux trash.

This assumes that a program called `trash' is in your $PATH and
that this program will, when passed a single file path as an
argument, move that file to the trash."
    (call-process trash-command nil nil nil filename)))

Use GNU df (gdf) on OSX if available

On OSX, the standard df command (BSD version, I think) is insufficient, and we want GNU df instead, which is typically installed as gdf. And we may as well use gdf over df on any other system which provides both as well. This implementation uses /opt/local/bin/gdf preferentially, since that is the version installed by Macports.

(when (executable-find "gdf")
  (setq directory-free-space-program "gdf"))

Fix region colors

Don’t let MacOS dark mode break region face

On Mac OS, Emacs uses ns_selection_fg_color and ns_selection_bg_color to determine the colors for the region face. When Mac OS activates dark mode, these colors change to dark blue, but they don’t change back when dark mode is deactivated (at least not in Emacs). So we avoid using these and instead set the colors exactly as they are in light mode.

(require 'frame)
(defun fix-mac-region-colors (&optional frame)
  "Set region face to a fixed color regardless of Mac Dark Mode

On Mac OS, Dark Mode messes with the color of the region in weird
ways that makes it visually unusable. Instead, we set the region
color to a static color that matches the non-dark-mode region
color."
  (interactive)
  (with-selected-frame (or frame (selected-frame))
           (when (and (equal (face-attribute 'region :distant-foreground)
                       "ns_selection_fg_color")
                (equal (face-attribute 'region :background)
                       "ns_selection_bg_color"))
       (set-face-attribute
        'region nil
        :distant-foreground 'unspecified
        :background "#BAD6FC"))))
;; Fix for future frames
(add-hook 'after-make-frame-functions #'fix-mac-region-colors)
;; Fix for current frames
(mapc #'fix-mac-region-colors (frame-list))

Don’t use excessively light region face in GTK on Ubuntu

In GTK on Ubuntu, the default selection colors are way too light, almost invisible. We instead choose a desaturated version of the standard Ubuntu orange selection color.

(require 'frame)
;; Lighter Ubuntu orange: #ffb499
;; Lighter GNOME blue: #88beff
(defvar gtk-region-custom-color "#88beff")

(defun fix-gtk-region-colors (&optional frame)
  "Set reasonable region colors on GNOME"
  (interactive)
  (with-selected-frame (or frame (selected-frame))
           (when (and (equal (face-attribute 'region :distant-foreground)
                             "gtk_selection_fg_color")
                      (equal (face-attribute 'region :background)
                             "gtk_selection_bg_color"))
             (set-face-attribute
              'region nil
              :distant-foreground 'unspecified
              :background gtk-region-custom-color))))
;; Fix for future frames
(add-hook 'after-make-frame-functions #'fix-gtk-region-colors)
;; Fix for current frames
(mapc #'fix-gtk-region-colors (frame-list))

About

My Emacs configuration (contents of ~/.emacs.d/)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published