NICEMACS

Home

nicemacs-logo.png

Table of Contents

Nicemacs v2

Preamble

;;; Nicemacs.v2 -*- lexical-binding: t -*-
;;; ==================================================================
;;
;;   ███╗   ██╗██╗ ██████╗███████╗███╗   ███╗ █████╗  ██████╗███████╗
;;   ████╗  ██║██║██╔════╝██╔════╝████╗ ████║██╔══██╗██╔════╝██╔════╝
;;   ██╔██╗ ██║██║██║     █████╗  ██╔████╔██║███████║██║     ███████╗
;;   ██║╚██╗██║██║██║     ██╔══╝  ██║╚██╔╝██║██╔══██║██║     ╚════██║
;;   ██║ ╚████║██║╚██████╗███████╗██║ ╚═╝ ██║██║  ██║╚██████╗███████║
;;   ╚═╝  ╚═══╝╚═╝ ╚═════╝╚══════╝╚═╝     ╚═╝╚═╝  ╚═╝ ╚═════╝╚══════╝
;;
;; To update your packages carry out the following steps:
;;
;; 1. M-x list-packages
;; 2. Press `r' to refresh
;; 3. Press `U' to mark upgradable packages
;; 4. Press `x' to execute the upgrades
;;
;;
;;  Maybe just: don't neglect the Elisp manual.  I've read it _far_
;;  more than the regular Emacs manual, and it's surprisingly fun.
;;  Seeing Emacs as an interactive lisp machine to be programmed
;;  rather than an editor to be configured makes many things easier.
;;
;;  ~ Tim Vaughan (2023)
;;
;; Packages used
;; -------------
;;
;; - `cl-lib'
;; - `copilot'
;; - `dired'
;; - `editorconfig' EditorConfig Emacs Plugin
;; - `ess' Emacs Speaks Statistics
;; - `evil' Extensible Vi layer for Emacs.
;; - `evil-collection' A set of keybindings for Evil mode
;; - `evil-leader' let there be <leader>
;; - `evil-mc'
;; - `evil-mc-extras' Extra functionality for evil-mc
;; - `evil-surround'
;; - `flyspell'
;; - `hl-todo' Highlight TODO and similar keywords
;; - `htmlize' Convert buffer text and decorations to HTML.
;; - `ligature' Ligature support for Emacs.
;; - `lorem-ipsum' Insert dummy pseudo Latin text
;; - `magit' A Git porcelain inside Emacs.
;; - `markdown-mode' Major mode for Markdown-formatted text
;; - `multiple-cursors' Multiple cursors for Emacs.
;; - `pyvenv' Python virtual environment interface
;; - `quarto-mode' A (poly)mode for https://quarto.org
;; - `rainbow-mode' Colorize color names in buffers
;; - `realgud' A front-end for interacting debuggers
;; - `s' The long lost Emacs string manipulation library.
;; - `unfill' Do the opposite of fill-paragraph or fill-region
;; - `which-key' Display available keybindings in popup
;; - `writegood-mode' Polish up poor writing on the fly
;; - `yasnippet' Yet another snippet extension for Emacs
;; - `yasnippet-snippets' Collection of yasnippet snippets
;;
;; Changelog
;; ---------
;;
;; - 2024-07
;;   + Use `openwith' so that `dired' will open PDFs with Okular on
;;     click rather than the emacs PDF viewer.
;;
;; - 2024-05
;;   + Add binding of SPC-m-s-s to evaluate R source blocks in
;;     org-mode. (This is C-c C-c by default.)
;;   + Add some ESS keybindings to `markdown-mode' so they are available when
;;     writing Rmarkdown.
;;   + Bug fix where ESS couldn't find the directory where the ESS-R
;;     files are. This is now fixed by looking up a plausible
;;     directory and setting `ess-etc-directory' to that.
;;
;; - 2024-03
;;   + Use `org-agenda-start-with-log-mode' to show the log mode items
;;     in the agenda.
;;   + `.Rmd' files should also open in `markdown-mode'.
;;   + Use the `:exclude' pattern to be a bit more selective with the
;;     files that get copied by `org-publish' (i.e. do not
;;     accidentally copy a whole Python virtual environment.)
;;
;; - 2024-02
;;   + Tweak `revert-without-query' to make it easier to use PNG files
;;
;; - 2024-01
;;   + Add `yaml-mode'.
;;   + Add `snakemake-mode' for Snakemake in Python.
;;   + Introduce a new org-mode todo state: SEMINAR
;;   + Update publishing functionality.
;;   + Add `python' to the list of languages used by Babel.
;;
;; - 2023-12
;;   + Abbreviate the eshell prompt in a nice way.
;;   + Include emoji keybinding amount yasnippet bindings since they
;;     play a similar role.
;;
;; - 2023-11
;;   + Refactor window selection.
;;   + Configure size of latex previews in org-mode.
;;   + Clean up snippet configuration.
;;
;; - 2023-10
;;   + Add a scrartcl class to `org-latex-classes' as nicer org-mode
;;     PDF export option.
;;
;; - 2023-09
;;   + Use `evil-window-X' functions as a simpler alternative to
;;     `winum'.
;;   + Use `IBuffer' to manage buffers.
;;   + Use `avy' to jump to lines in a buffer using a character.
;;
;; - 2023-08
;;   + Use `indent-guide' to visualise indentation in `python-mode'.
;;   + Use `org-agenda-window-setup' to open agenda in a new frame.
;;   + Use `org-log-done' to record when tasks where completed.
;;   + Upgrade to Emacs 29.1 and enable smooth scrolling.
;;
;; - 2023-07
;;   + Re-organised the themeing and started Leuven.
;;   + Isolate theme configuration so it is easier to edit.
;;   + Set up debugging with `realgud' (for Python).
;;   + Improve window management.
;;   + Set up a major mode for MATLAB.
;;   + Introduce a new org-mode todo states.
;;   + Avoid annoying warning from `git' by setting `GIT_PAGER' to
;;     `cat'.
;;
;; - 2023-06
;;   + Include the `cdf' function for easier eshell navigation.
;;   + Include keys for ESS devtools integration.
;;   + Include wrapper around `pp-emacs-lisp-code'.
;;   + Add some configuration for `nxml-mode'.
;;   + Start using `polymode' (and friends) so I can write R with from
;;     within `org-mode'.
;;   + Configure the `fill-column' to use a clearer face.
;;
;; - 2023-05
;;   + Use the `use-package' macro.
;;   + Use the `calfw' package for a calendar view of my agenda
;;   + Remove racket/scheme pacakge
;;   + Include option to accept AI suggestions line by line.
;;   + Include a linter function for R (using `formatR').
;;   + Use the `unfill' package for better paragraph un/filling.
;;   + Use the `lorem-ipsum' package for convenient dummy text.
;;   + Include python configuration.
;;
;; - 2023-04
;;   + Set up an org-mode file for documentation with tangling and
;;     detangling.
;;   + Use `S <x>` in visual mode to surround the region in <x>.
;;   + Use `SPC b s <x>` to open a scratch buffer in mode<x>
;;   + Edit `message-buffer-file-name' so it works in `dired'.
;;   + Extend `before-save-hook' to avoid accidental trailing
;;     whitespace.
;;   + Use JetBrains Mono as the font with ligatures.
;;
;;; ==================================================================

STUFF 1

(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(package-initialize)

(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

(eval-when-compile
  (require 'use-package))
(require 'bind-key)

(setq user-full-name "Alexander E. Zarebski")

(defvar nice-journal-directory "~/Documents/journal/"
  "The directory for nicemacs journal files.")
(defvar nice-notes-directory "~/public-site/org/notes"
  "The directory for nicemacs notes files.")
(defvar nice-website-directory "~/aezarebski.github.io/"
  "The directory for my website.")
(defvar nice-resources-dir "~/Documents/nicemacs/resources"
  "The path to the nicemacs resources directory on this machine.")
(defvar nice-nicemacs-directory "~/Documents/nicemacs"
  "The path to the nicemacs directory on this machine.")
(defvar nice-snippet-directory "~/.emacs.d/snippets"
  "The path to the whipper snipper directory on this machine.")

(use-package cl-lib
  :ensure t)

Evil

Setting the scroll-margin to 2 will start scrolling when the point is one line from the top or bottom of the window and setting scroll-conservatively to a value greater than 100 means that it will keep the point in place while scrolling (which gives a smoother scroll).

;; Be evil
;; -------
;;
;; Evil surroundings
;;
;; 1. Enter visual mode and select the text as the region.
;; 2. Press `S'.
;; 3. Type the symbol to surround it (note, if it is part of a opening
;;    and closing pair, the opening includes a space and the closing
;;    does not.)
;;

(setq evil-want-keybinding nil)

(use-package evil
  :ensure t
  :init
  (evil-mode 1))

(use-package evil-leader
  :ensure t
  :config
  (evil-leader-mode 1)
  (global-evil-leader-mode 1)
  (evil-leader/set-key "t s" 'evil-surround-mode)
  (evil-leader/set-leader "<SPC>")
  (evil-leader/set-key "<SPC>" 'execute-extended-command))

(use-package evil-collection
  :ensure t
  :config
  (evil-collection-init))

(use-package evil-surround
  :ensure t
  :config
  (global-evil-surround-mode 1))

(use-package avy
  :ensure t
  :config
  (evil-leader/set-key "J" 'avy-goto-line))

Appearance

Fonts

;; Fonts
;; -----
;;
;; To install JetBrains Mono, or any other font, follow these steps:
;;
;; 1. Download and extract the font, you should have a "ttf" directory
;;    containing the font files.
;; 2. Create a font directory if you don't already have one
;;    $ mkdir -p ~/.local/share/fonts
;; 3. Copy the font files to the font directory:
;;    $ cp path/to/extracted/ttf/*.ttf ~/.local/share/fonts
;; 4. Update the font cache:
;;    $ fc-cache -f -v
;;
(set-frame-font "JetBrains Mono" nil t)
(ligature-set-ligatures 'prog-mode '("|>" "<-" "<<-" "==" "!=" ">=" "<="))
(global-ligature-mode nil)

(defun toggle-ligatures ()
  "Toggle ligatures on and off."
  (interactive)
  (if (bound-and-true-p global-ligature-mode)
      (global-ligature-mode -1)
    (global-ligature-mode 1)))
(evil-leader/set-key "t l" 'toggle-ligatures)

General

;; Look stunning
;; -------------
;;
;; `pixel-scroll-precision-mode' means you can have smooth scrolling
;; if you have a compatible mouse.
;;

(tool-bar-mode -1)                     ; remove the tool bar

(pixel-scroll-precision-mode 1)
(setq pixel-dead-time 0)

(setq scroll-margin 2
      scroll-conservatively 101
      scroll-preserve-screen-position 1)

(setq-default scroll-bar-width 10)
(setq-default left-fringe-width 10)
(setq-default right-fringe-width 10)

;; I dislike trailing whitespace creeping into my files so the
;; following will make it visible and automatically remove it upon
;; saving. NOTE setting `show-trailing-whitespace' globally leads to
;; some things being highlighted in other buffers such as `calendar'
;; where they should not be hightlight. Doing it with
;; `nice-show-trailing-whitespace' ensures it is set locally as
;; appropriate.
(defun nice-show-trailing-whitespace ()
  "Enable trailing whitespace highlighting only when editing a file."
  (setq show-trailing-whitespace (buffer-file-name)))
(add-hook 'find-file-hook 'nice-show-trailing-whitespace)
(add-hook 'before-save-hook 'delete-trailing-whitespace)

(use-package hl-todo
  :ensure t
  :config
  (global-hl-todo-mode)
  (global-hl-line-mode t))

(defun boxed-face (colour &optional background line-width)
  "Create a face with a specified foreground COLOUR and optional BACKGROUND.

   If LINE-WIDTH is not specified, it defaults to 1.
   This face will be bold and boxed with the same colour as the foreground."
  (let ((width (or line-width 1)))
    `((t (:foreground ,colour
          :weight bold
          :background ,background
          :box (:line-width ,width
                :color ,colour))))))

(setq hl-todo-keyword-faces
      `(("TODO"   . ,(boxed-face "red" "#ffc8c8"))
        ("FIXME"  . ,(boxed-face "magenta"))
        ("NOTE"   . ,(boxed-face "cyan"))
        ("DONE"   . ,(boxed-face "blue" "#E6ECFF"))))

(setq fill-column 70)

(defun nice-toggle-fill-column-indicator ()
  "Toggle display of the fill column indicator.

When active, the indicator is set to a vertical line. It also
turns on `display-fill-column-indicator-mode' if it's not already
active, and turns it off if it is."
  (interactive)
  (display-fill-column-indicator-mode 'toggle)
  (when display-fill-column-indicator-mode
    (setq display-fill-column-indicator-character ?\u2502)
    (set-face-attribute 'fill-column-indicator nil
                        :foreground "magenta"
                        :weight 'bold)))

(evil-leader/set-key "t f" 'nice-toggle-fill-column-indicator)

Theme: Leuven

(setq nice-light-theme 'leuven
      nice-dark-theme 'leuven-dark)

(load-theme nice-light-theme t)

(defun nice-toggle-theme ()
  "Toggle between my light and dark themes."
  (interactive)
  (if (eq (car custom-enabled-themes) nice-light-theme)
      (progn
        (disable-theme nice-light-theme)
        (load-theme nice-dark-theme t))
    (progn
      (disable-theme nice-dark-theme)
      (load-theme nice-light-theme t))))

(evil-leader/set-key "t t" 'nice-toggle-theme)

Theme: Solarized   EXCLUDED

(setq nice-colours-alist
      '((strong-warning . "red")
        (weak-warning . "magenta")
        (weak-note . "cyan")
        (strong-note . "blue")
        (light-theme-comment-background . "#e4ecda")
        (light-theme-comment-foreground . "#207e7b")
        (light-theme-shadow-background . "#eee8d5")
        (light-theme-shadow-foreground . "#93a1a1")
        (dark-theme-comment-background . "#207e7b")
        (dark-theme-comment-foreground . "#e4ecda")
        (dark-theme-shadow-background . "#202c2a")
        (dark-theme-shadow-foreground . "#254d48")))

(defun nice-colour (colour)
  "Return the colour associated with the symbol COLOUR."
  (cdr (assoc colour nice-colours-alist)))


(set-face-attribute 'hl-line nil
                    :background (nice-color 'light-theme-shadow-background)

(add-to-list `custom-theme-load-path "~/.emacs.d/themes/")
(load-theme 'solarized-light-high-contrast t)

(defun nice-set-theme (theme comment-bg comment-fg shadow-bg shadow-fg)
  (load-theme theme t)
  (let ((comment-face `((t (:background ,comment-bg
                                        :foreground ,comment-fg
                                        :slant normal)))))
    (setq font-lock-comment-delimiter-face comment-face)
    (setq font-lock-comment-face comment-face))
  (set-face-background 'mode-line comment-bg)
  (set-face-foreground 'mode-line comment-fg)
  (set-face-background 'mode-line-inactive shadow-bg)
  (set-face-foreground 'mode-line-inactive shadow-fg))

(defun nice-toggle-themes ()
  "Toggle between two themes: solarized-light-high-contrast and
solarized-dark-high-contrast and adjust the comment face to one
that is visible in both."
  (interactive)
  (if (eq (car custom-enabled-themes) 'solarized-light-high-contrast)
      (progn
        (disable-theme 'solarized-light-high-contrast)
        (nice-set-theme 'solarized-dark-high-contrast
                        (nice-colour 'dark-theme-comment-background)
                        (nice-colour 'dark-theme-comment-foreground)
                        (nice-colour 'dark-theme-shadow-background)
                        (nice-colour 'dark-theme-shadow-foreground)))
    (progn
      (disable-theme 'solarized-dark-high-contrast)
      (nice-set-theme 'solarized-light-high-contrast
                      (nice-colour 'light-theme-comment-background)
                      (nice-colour 'light-theme-comment-foreground)
                      (nice-colour 'light-theme-shadow-background)
                      (nice-colour 'light-theme-shadow-foreground)))))

(evil-leader/set-key "t t" 'nice-toggle-themes)

Other

;; Rainbow-mode will highlight strings indicating colours,
;; e.g. hexcodes in their corresponding colour.
(use-package rainbow-mode
  :ensure t
  :hook ((emacs-lisp-mode . rainbow-mode)
         (ess-mode . rainbow-mode)))

(setq inhibit-splash-screen t)

(evil-leader/set-key
  "z j" 'text-scale-decrease
  "z k" 'text-scale-increase)

;; Be sensible
;; -----------

(use-package unfill
  :ensure t
  :bind ("M-q" . unfill-toggle))

(evil-leader/set-key
  "q r" 'restart-emacs
  "q q" 'save-buffers-kill-emacs)

;; Frame related commands will have keys starting with `F'.
(evil-leader/set-key "F f" 'toggle-frame-fullscreen)

(defun nice-pop-out-window ()
  "Pop the current window out into a new frame.

If there is only a single window then do nothing because it is
already in its own frame."
  (interactive)
  (unless (one-window-p)
    (let ((current-buffer (current-buffer)))
      (delete-window)
      (display-buffer-pop-up-frame current-buffer nil))))

(evil-leader/set-key "F p" 'nice-pop-out-window)

Which-key

Get information on partial keys to help you remember/discover functionality.

;; The which-key package is a great way to be reminded of what keys
;; are available from the start of a key sequence.
(use-package which-key
  :ensure t
  :config
  (which-key-mode)
  (setq which-key-idle-delay 0.3))

(setq key-description-pairs
      '(("SPC a" . "Agenda (org-mode)")
        ("SPC b" . "Buffers/Babel")
        ("SPC c" . "Cursors")
        ("SPC c" . "Delete")
        ("SPC f" . "Files/Dired")
        ("SPC F" . "Frame")
        ("SPC g" . "Git (magit)")
        ("SPC g c" . "Commits")
        ("SPC H" . "HELP!!!")
        ("SPC m" . "Major")
        ("SPC m v" . "EnVironment")
        ("SPC m d" . "devtools (ESS)")
        ("SPC m s" . "REPL (prog)/Sort (dired)")
        ("SPC m c" . "Code lint/format")
        ("SPC q" . "Quit/Exit")
        ("SPC s" . "Shell/Search")
        ("SPC S" . "Spelling")
        ("SPC t" . "Toggles")
        ("SPC v" . "Visitors")
        ("SPC v b" . "Bibtex")
        ("SPC v f" . "Files")
        ("SPC v d" . "Directories")
        ("SPC w" . "Windows")
        ("SPC y" . "Yasnippet")
        ("SPC z" . "Zoom (without a mouse)")))

(dolist (pair key-description-pairs)
  (which-key-add-key-based-replacements (car pair) (cdr pair)))

Diff-ing files

Meld provides a convenient GUI based method for assessing the differences between files.

;; Diffing files with meld
;; -----------------------
;;
;; + f m a - will diff the aspell dictionary
;; + f m i - will diff the emacs init
;; + f m m - will prompt for two files to diff
;;
(defmacro nice-meld-files (name fa fb key)
  "Generate function named nice-meld-NAME which opens meld diff for
files FA and FB using SPC f m KEY."
  `(progn
     (defun ,(intern (format "nice-meld-%s" name)) ()
       (interactive)
       (async-shell-command ,(format "meld %s %s &" fa fb)))
     (evil-leader/set-key ,(concat "f m " key) (intern ,(format "nice-meld-%s" name)))))

(nice-meld-files "init" "~/.emacs.d/init.el"
                 "~/Documents/nicemacs/nicemacs-v2.el"
                 "i")
(nice-meld-files "aspell" "~/.aspell.en.pws"
                 "~/Documents/nicemacs/resources/aspell.en.pws"
                 "a")

(defun nice-meld ()
  "Prompt for two files and show the difference between them using
`meld`."
  (interactive)
  (let ((file1 (read-file-name "First file: "))
        (file2 (read-file-name "Second file: ")))
    (shell-command (format "meld %s %s &" file1 file2))))

(evil-leader/set-key "f m m" 'nice-meld)

Window management

Number based solution from winum   EXCLUDED

;; The `winum' package facilitates switching between windows using
;; numbers which appear in the bottom left hand of the window, at the
;; start of the mode-line.
(use-package winum
  :ensure t
  :config
  (winum-mode)
  (setq winum-format " %s "))

(set-face-attribute 'winum-face nil
                    :foreground "black"
                    :background "gold"
                    :weight 'bold
                    :underline nil
                    :height 1.1)
(evil-leader/set-key
  "0" 'winum-select-window-0
  "1" 'winum-select-window-1
  "2" 'winum-select-window-2
  "3" 'winum-select-window-3
  "4" 'winum-select-window-4
  "5" 'winum-select-window-5
  "w a" 'nice-balance-windows-alt
  "w b" 'balance-windows
  "w n s" 'nice-swap-buffers)

(defun nice-swap-buffers ()
  "Swap buffers between two windows specified by their numbers.

Prompt for two window numbers and swap the buffers displayed in
those windows. Window numbers are assigned by the `winum' package."
  (interactive)
  (let* ((win1 (winum-get-window-by-number
                (read-number "First window number: ")))
         (win2 (winum-get-window-by-number
                (read-number "Second window number: ")))
         (buffer1 (and win1 (window-buffer win1)))
         (buffer2 (and win2 (window-buffer win2))))
    (when (and buffer1 buffer2)
      (set-window-buffer win1 buffer2)
      (set-window-buffer win2 buffer1))))

Evil window management

(defmacro define-nice-window-move (name move-func)
  `(defun ,name ()
     (interactive)
     (,move-func 1)
     (let ((ov (make-overlay (point-min) (point-max))))
       (overlay-put ov 'window (selected-window))
       (overlay-put ov 'face '(:background "magenta"))
       (sit-for 0.1)
       (delete-overlay ov))))

(define-nice-window-move nice-window-up evil-window-up)
(define-nice-window-move nice-window-down evil-window-down)
(define-nice-window-move nice-window-left evil-window-left)
(define-nice-window-move nice-window-right evil-window-right)

(evil-leader/set-key
  "k" 'nice-window-up
  "j" 'nice-window-down
  "h" 'nice-window-left
  "l" 'nice-window-right
  "w a" 'nice-balance-windows-alt
  "w b" 'balance-windows
  "w s" 'split-window-below
  "w v" 'split-window-right
  "w L" 'evil-window-move-far-right
  "w H" 'evil-window-move-far-left
  "w J" 'evil-window-move-very-bottom
  "w K" 'evil-window-move-very-top)

(defun nice-balance-windows-alt ()
  "Balance windows such that the current window receives a certain
amount of the of the frame's width and height."
  (interactive)
  (balance-windows)
  (let* ((proportion 0.7)
         (frame-width (frame-width))
         (frame-height (frame-height))
         (desired-width (floor (* proportion frame-width)))
         (desired-height (floor (* proportion frame-height))))
    (enlarge-window-horizontally (- desired-width (window-width)))
    (enlarge-window (- desired-height (window-height)))))

Shells

;; Shell stuff
;; -----------
;;
(defun nice-eshell ()
  "Open an existing or new eshell buffer in a vertical split."
  (interactive)
  (let ((eshell-buffer (get-buffer "*eshell*"))
        (width (/ (frame-width) 2)))
    (split-window-horizontally)
    (other-window 1)
    (window-resize nil (- width (window-width)) t)
    (if eshell-buffer
        (switch-to-buffer eshell-buffer)
      (eshell))))

(defun nice-eshell-prompt ()
  (let* ((directory (abbreviate-file-name (eshell/pwd)))
         (parent (file-name-directory directory))
         (name (file-name-nondirectory directory))
         (base-prompt (concat (if parent
                                  (concat "... " (file-name-nondirectory (directory-file-name parent)) "/")
                                "")
                              name
                              " $ "))
         (trimmed-prompt (if (> (length base-prompt) 50)
                             (concat "[...] " (substring base-prompt (- (length base-prompt) 44)))
                           base-prompt)))
    (if (string-match-p "~" trimmed-prompt)
        (replace-regexp-in-string "^\\.\\.\\. " "" trimmed-prompt)
      trimmed-prompt)))

(setq eshell-prompt-function 'nice-eshell-prompt)

(setq eshell-cmpl-ignore-case t)
(evil-leader/set-key
  "s e" 'eshell
  "s b" (lambda () (interactive) (ansi-term "/bin/bash"))
  "s i" 'ielm
  "s r" 'R
  "'" 'nice-eshell)

(defun cdf (filepath)
  "Change the current directory in Eshell to the directory of
 FILEPATH."
  (let ((dir (file-name-directory filepath)))
    (when (file-directory-p dir)
      (eshell/cd dir))))

(defun nice-eshell-mode-setup ()
  (setenv "TERM" "dumb")
  (setenv "GIT_PAGER" "cat"))

(add-hook 'eshell-mode-hook 'nice-eshell-mode-setup)

Dired

Setting the dired-dwim-target variable to t means that dired will search for an appropriate directory to start from when you copy a file via dired. I usually have both directories in adjacent windows when moving files between them, so this is more convenient.

;; Dired
;; -----
;;
;; - R :: mv
;; - C :: cp
;; - + :: mkdir
;; - - :: cd ../
;; - m :: mark a file
;; - u :: unmark a file
;; - d :: flag file for deletion
;; - x :: execute deletion
;;
(use-package dired
  :bind (:map dired-mode-map
              ("-" . dired-up-directory))
  :config
  (setq dired-listing-switches "-alh")
  (setq dired-dwim-target t)
  (evil-leader/set-key-for-mode 'dired-mode "m s" 'dired-sort-toggle-or-edit))

Note that there is no :ensure t here. This is because the dired package is installed by default and is not no the package repositories, so if you have :ensure t it will throw a warning saying it is not installed in the expected way. Just removing :ensure t fixes it though.

By default you will always be prompted if you want to revert a file if you already are visiting it and it has changed on the file system since you last looked. This is a bit annoying when you are working on figures. The following modication of revert-without-query ensures no confirmation is required when opening PNGs.

(add-to-list 'revert-without-query "\\.png$")

(use-package openwith
  :ensure t
  :config
  (setq openwith-associations
        (list (list (openwith-make-extension-regexp '("pdf"))
                    "okular"
                    '(file))))
  (openwith-mode t))

The use of the openwith package above means that when you click on a PDF in dired, it will open with Okular, rather than in the DocViewer in Emacs.

Searching

The following advice from the emacs manual may be useful if you are trying to locate some files.

To search for files with names matching a wildcard pattern use M-x find-name-dired. It reads arguments DIRECTORY and PATTERN, and chooses all the files in DIRECTORY or its subdirectories whose individual names match PATTERN.

There is also the grep command for searching within files and the find command for searching based on the filename.

Buffers, files, and dired

You can get a list of all the current buffers with ibuffer. Important keys for Ibuffer include

  • d to mark for killing and x to run those kills,
  • g r to refresh the listing,
  • o <thing> to sort by:
    • o v time
    • o m mode
    • o a name (alphbetical)
    • o i to invert the ordering.
  • and u to unmark buffers.
;; Buffer stuff
;; ------------

(evil-leader/set-key
  "b r" 'revert-buffer
  "b l" 'ibuffer)

(defface ibuffer-modified-buffer
  '((t (:foreground "white"
        :weight bold
        :background "red")))
  "Face used for highlighting unsaved buffers in IBuffer.")

;; Declare that IBuffer should use the `ibuffer-modified-buffer' face
;; for modified buffers so that they stand out.
(add-hook 'ibuffer-mode-hook
          (lambda ()
            (ibuffer-auto-mode 1)
            (ibuffer-switch-to-saved-filter-groups "default")
            (add-to-list 'ibuffer-fontification-alist
                         '(0 (buffer-modified-p) 'ibuffer-modified-buffer))))

;; File stuff
;; ----------

(evil-leader/set-key
  "f f" 'find-file
  "f l" 'find-file-literally
  "f t" 'nice-touch-file
  "f F" 'find-file-other-frame
  "f s" 'save-buffer
  "f d" 'nice-dired
  "b b" 'switch-to-buffer
  "d b" 'kill-buffer
  "d w" 'delete-window
  "d F" 'delete-frame
  "F d" 'delete-frame)

(defun nice-dired ()
  "Open dired for the current buffer's directory if it
 corresponds to a file, the working directory of the shell if
 the current buffer is a shell, or the home directory otherwise."
  (interactive)
  (let* ((buffer-mode (with-current-buffer (current-buffer) major-mode))
         (dir (cond ((buffer-file-name)
                     (file-name-directory (buffer-file-name)))
                    ((or (eq buffer-mode 'term-mode)
                         (eq buffer-mode 'eshell-mode)
                         (eq buffer-mode 'inferior-ess-r-mode))
                     (with-current-buffer (if (eq buffer-mode 'inferior-ess-r-mode)
                                              (process-buffer (ess-get-process ess-current-process-name))
                                            (current-buffer))
                       (file-name-directory default-directory)))
                    (t (expand-file-name "~/")))))
    (dired dir)))

(defun nice-touch-file ()
  "In the current dired buffer touch a new file with a name
retreived from the prompt."
  (interactive)
  (if (not (eq major-mode 'dired-mode))
      (error "Not in dired mode"))
  (let ((filename (read-string "Filename: ")))
    (shell-command (format "touch %s" filename))
    (revert-buffer)))

(defmacro nice-scratch-buffer (mode key)
  "Create a nice-scratch-buffer function for MODE and bind it to KEY."
  (let ((func-name (intern (format "nice-scratch-buffer-%s" (symbol-name mode))))
        (docstring (format "Open the scratch buffer and set the major mode to `%s'." mode)))
    `(progn
       (defun ,func-name ()
         ,docstring
         (interactive)
         (switch-to-buffer "*scratch*")
         (,mode))
       (evil-leader/set-key ,key ',func-name))))
(nice-scratch-buffer text-mode "b s t")
(nice-scratch-buffer org-mode "b s o")
(nice-scratch-buffer emacs-lisp-mode "b s e")

STUFF 2

;; Consult the oracle
;; ------------------

(evil-leader/set-key
  "H s" 'apropos
  "H d b" 'message-buffer-file-name
  "H d f" 'describe-function
  "H d m" 'describe-mode
  "H d p" 'describe-package
  "H d k" 'describe-key
  "H d v" 'describe-variable)

(defun message-buffer-file-name ()
  "Print the full path of the current buffer's file or directory to the
minibuffer and store this on the kill ring."
  (interactive)
  (let ((path (or buffer-file-name
                  (and (eq major-mode 'dired-mode)
                       (dired-current-directory)))))
    (when path
      (kill-new path)
      (message path))))

(defun message-link-at-point ()
  "Print the full path of a link at the point so we know where this
will take us."
  (interactive)
  (let* ((link (org-element-context))
         (link-file-name (org-element-property :path link)))
    (when (eq (org-element-type link) 'link)
      (kill-new link-file-name)
      (message "%s" link-file-name))))

(evil-leader/set-key "H l m" 'message-link-at-point)

;; Learn from your past
;; --------------------

(defmacro nice-rgrep-directory (dname path pattern key)
  "Create a function that calls `rgrep' on the specified DIRECTORY
and binds it to a KEY.

DNAME is the name of the directory used to generate the function
name.
PATH is the path to the directory to be searched.
KEY is the keybinding (as a string) to trigger the rgrep function."
  `(progn
     (defun ,(intern (format "nice-rgrep-%s" dname)) ()
       ,(format "Search for a string in %s using rgrep." dname)
       (interactive)
       (rgrep (read-string "Search terms: ") ,pattern ,path))
     (evil-leader/set-key ,(concat "s g " key) (intern ,(format "nice-rgrep-%s" dname)))))

(nice-rgrep-directory "website" "~/public-site/org" "*" "w")
(nice-rgrep-directory "notes" "~/public-site/org/notes" "*" "n")
(nice-rgrep-directory "journal" "~/Documents/journal" "*.org" "j")
(nice-rgrep-directory "reviews" "~/Documents/bibliography" "*" "r")

(evil-leader/set-key "s g ." (lambda ()
                               (interactive)
                               (rgrep (read-string "Search terms: ")
                                      "*")))

;; Be virtuous and lead by example
;; ===============================

(setq-default major-mode
              (lambda ()
                (unless buffer-file-name
                  (let ((buffer-file-name (buffer-name)))
                    (set-auto-mode)))))
(setq confirm-kill-emacs #'yes-or-no-p)
(recentf-mode t)

(setq read-buffer-completion-ignore-case t
      read-file-name-completion-ignore-case t
      completion-ignore-case t)


;; Be powerful with packages
;; =========================

;; Obfuscate the text on the screen if there is no movement for 60
;; seconds.
(require 'zone)
(zone-when-idle 0)
(setq zone-programs [zone-pgm-whack-chars])
(evil-leader/set-key "z z" 'zone)


;; NXML
;; ----

;; u - up to parent.
;; p - previous tag.
;; n - next tag.
(evil-leader/set-key-for-mode 'nxml-mode
  "m u" 'nxml-backward-up-element
  "m p" 'nxml-backward-element
  "m n" 'forward-sexp)

Yasnippet

I have a collection of yasnippets here. To use these snippets clone that repository into your .emacs.d/ under the name snippets and use yas-reload-all (possibly after yas-recompile-all although this can be slow).

;; Yasnippet
;; ---------
;;
;; See https://github.com/aezarebski/whipper-snipper
;;

(use-package yasnippet
  :ensure t
  :config
  (yas-global-mode 1))

(defun nice-load-snippets ()
  "Load the snippets in my snippet directory"
  (interactive)
  (let ((snippets-dir nice-snippet-directory))
    (unless (file-exists-p snippets-dir)
      (make-directory snippets-dir))
    (yas-load-directory snippets-dir)))

(nice-load-snippets)

(evil-leader/set-key
  "y i" 'yas-insert-snippet     ; Insert a snippet
  "y n" 'yas-new-snippet        ; Create a new snippet
  "y v" 'yas-visit-snippet-file ; Visit the snippet file for the current mode
  "y r" 'yas-reload-all         ; Reload all snippets
  "y c" 'yas-compile-directory  ; Compile all snippets
  "y l" 'nice-load-snippets     ; Load your custom snippets
  "y g" 'nice-go-to-snippets-dir
  "y e" 'emoji-list)

(defun nice-go-to-snippets-dir ()
  "Open the snippets directory in dired."
  (interactive)
  (dired nice-snippet-directory))

STUFF 3

;; Multiple cursors
;; ----------------
;;
;; Using mutiple cursors is a little bit tricky but here are some
;; simple steps you can try on the following example text.
;;
;; ```
;; the cat sat on the mat
;; catch this ball said pat
;; the food was eaten by the cat
;; ```
;;
;; 1. Select the an instance of "cat" with the cursor at the start
;; 2. Use the keys below, e.g. `SPC c n` to select occurrences
;; 3. Use `evil-insert' (`SPC c i`) to start editing.
;; 4. Exit using `mc/keyboard-quit' (`SPC c q`)

(use-package multiple-cursors
  :ensure t)

(use-package evil-mc
  :ensure t
  :config (global-evil-mc-mode 1))

(evil-leader/set-key
  "c n" 'mc/mark-next-like-this        ; Mark next occurrence
  "c p" 'mc/mark-previous-like-this    ; Mark previous occurrence
  "c N" 'mc/skip-to-next-like-this     ; Skip and mark next occurrence
  "c P" 'mc/skip-to-previous-like-this ; Skip and mark previous occurrence
  "c u" 'mc/unmark-next-like-this      ; Unmark next cursor
  "c U" 'mc/unmark-previous-like-this  ; Unmark previous cursor
  "c i" 'evil-insert                   ; Drop into using the cursors
  "c q" 'mc/keyboard-quit              ; Quit multiple-cursors mode
  )

Magit

Staging and unstaging multiple files

You can select multiple files to unstage in one go using the region. To do this, follow these steps:

  1. Navigate to the "Staged changes" section in the Magit status buffer.
  2. Move the cursor to the first file you want to unstage.
  3. Set the mark by pressing C-SPC (Control + Space).
  4. Move the cursor to the last file you want to unstage. This will create a region that includes all the files you want to unstage.
  5. Press u to unstage all the files in the region.

You can also use the same method to stage multiple files in the "Unstaged changes" section. Just follow the same steps, but press s instead of u in step 5 to stage the files in the region.

Configuration

;; Magit
;; -----
(use-package magit
  :ensure t
  :config
  (setq magit-display-buffer-function
        #'magit-display-buffer-fullframe-status-v1)
  (setenv "SSH_AUTH_SOCK" "<ADD THE CORRECT PATH HERE>")
  (evil-leader/set-key
    "g s" 'nice/magit-status
    "g q" 'with-editor-cancel))

The following is meant to solve the problem where the window configuration is lost by magit-status. I haven't been able to get the bindings working within magit-status though, so you have to call nice/magit-quit manually. It does restore the configuration though which is nice.

(defvar nice/temp-window-configuration nil
  "Temporary variable to hold the window configuration.")

(defun nice/magit-status ()
  "Save the current window configuration and open Magit status."
  (interactive)
  (setq nice/temp-window-configuration (current-window-configuration))
  (magit-status))

(defun nice/magit-quit ()
  "Restore the window configuration from before opening Magit status."
  (interactive)
  (when nice/temp-window-configuration
    (set-window-configuration nice/temp-window-configuration)
    (setq nice/temp-window-configuration nil)))

In the use-package command above we set the SSH_AUTH_SOCK manually which fixes some weird issue where git was not able to authenticate correctly. This only happened on one machine and is probably machine specific so you'll need to be careful to get the right value here.

(defmacro nice-canned-commit-message (fname cmessage key)
  "Define a canned commit message function with an Evil key binding.

  This macro takes in three arguments:
  - FNAME: A string that will be used to construct the function name.
  - CMESSAGE: A string that represents the canned commit message.
  - KEY: A string that represents the keybinding for the function using the Evil leader.

  The function created by this macro generates a commit message with a timestamp by
  concatenating the specified CMESSAGE string with the current day and time. The commit
  is created using `magit-commit-create', which is invoked with the `--edit` option to
  open the commit message in an editor. The function is bound to the Evil leader key
  sequence `g c KEY`, where `KEY` is the specified key string.

  Example usage:
  (nice-canned-commit-message \"my-canned-commit\" \"Fix some bugs\" \"c\")"
  `(progn
     (defun ,(intern (format "nccm-%s" fname)) ()
       "Generate a canned commit message with a timestamp."
       (interactive)
       (let ((commit-message (format "%s %s"
                                     ,cmessage
                                     (downcase (format-time-string "%A %l:%M %p")))))
         (magit-commit-create (list "--edit" (concat "-m \"" commit-message "\"")))))
     (evil-leader/set-key ,(concat "g c " key) (intern ,(format "nccm-%s" fname)))))

(nice-canned-commit-message emacs "update emacs config" "e")
(nice-canned-commit-message flashcards "flashcards" "f")
(nice-canned-commit-message journal "update journal" "j")
(nice-canned-commit-message notes "update notes" "n")
(nice-canned-commit-message review "update reading list" "r")
(nice-canned-commit-message website "update website" "w")
(nice-canned-commit-message yasnippet "yasnippet" "y")

Emacs lisp

  • Awesome Elisp sounds like it would be a good place to go to learn a bit more elisp.

The pp-sexp-to-kill-ring function is there to help pretty print code. It uses a new pretty printer function included in 29.1 and puts the pretty-printed version of an S-expression on the kill ring.

;; Emacs Lisp
;; ----------

(setq pp-max-width 70)
(setq pp-use-max-width t)

(defun pp-sexp-to-kill-ring ()
  "Pretty-print the S-expression under the cursor and add it to the
kill ring."
  (interactive)
  (let ((sexp (read (thing-at-point 'sexp)))
        (temp-buffer (generate-new-buffer "*temp*")))
    (with-current-buffer temp-buffer
      (pp-emacs-lisp-code sexp)
      (kill-new (buffer-string)))
    (kill-buffer temp-buffer)))

(evil-leader/set-key-for-mode 'emacs-lisp-mode
  "m s c" 'eval-last-sexp
  "m s b" 'eval-buffer
  "m s r" 'eval-region
  "m c l" 'pp-sexp-to-kill-ring)

Emacs Speaks Statistics (ESS)

;; Emacs Speaks Statistics (ESS)
;; -----------------------------

(use-package ess
  :ensure t
  :init
  (setq ess-etc-directory (concat (car (directory-files "~/.emacs.d/elpa/" t "ess-[0-9]+")) "/etc/"))
  :mode ("\\.Rmd" . Rmd-mode)
  :config
  (setq ess-default-style 'DEFAULT
        ess-history-file nil)
  (evil-leader/set-key-for-mode 'ess-r-mode
    "m d t" 'ess-r-devtools-test-package
    "m d l" 'ess-r-devtools-load-package
    "m d b" 'ess-r-devtools-build
    "m d i" 'ess-r-devtools-install-package
    "m d c" 'ess-r-devtools-check-package
    "m d d" 'ess-r-devtools-document-package
    "m s b" 'ess-eval-buffer
    "m s r" 'ess-eval-region
    "m s c" 'ess-eval-region-or-line-visibly-and-step
    "m s s" 'ess-eval-region-or-function-or-paragraph-and-step
    "m c l" 'nice-code-lint-buffer-r
    "m c i" 'indent-region
    "m '" 'ess-switch-to-inferior-or-script-buffer))

(use-package quarto-mode
  :ensure t)

(defun nice-code-lint-buffer-r ()
  "Lint the current R buffer using lintr."
  (interactive)
  (ess-eval-linewise "library(lintr)\n")
  (ess-eval-linewise (format "print(lint(\"%s\"))\n" buffer-file-name)))

MATLAB   EXCLUDED

;; MATLAB
;; ------
;;
;; TODO There should be a variable for the `nice-packages' directory.
;;

(use-package matlab-load
  :load-path "~/.emacs.d/nice-packages/matlab-emacs-src"
  :config
  (setq matlab-indent-function t)
  (setq matlab-shell-command "~/MATLAB/bin/matlab"))

(evil-leader/set-key-for-mode 'matlab-mode
  "m s b" 'matlab-shell-save-and-go
  "m s r" 'matlab-shell-run-region
  "m '" 'matlab-show-matlab-shell-buffer)

RealGUD debugging

;; Debugging
;; ---------
;;
;; Commands
;;   - `n' next line
;;   - `s' step into expression
;;   - `c' continue
;;   - `l' list context
;;   - `p' print variable
;;   - `q' quit debugger
;;
;; Debug a Python script by
;;   1. adding `import pdb; pdb.set_trace()'
;;   2. running the script with `realgud:pdb'
;;

(use-package realgud
  :ensure t
  :config
  (setq realgud:pdb-command-name "python -m pdb"))

Python

;; Python
;; ------
;;
;; Use `pyvenv-activate' to activate a virtual environment.

(use-package pyvenv
  :ensure t)

(use-package python
  :ensure t
  :config
  (setq python-shell-interpreter "python3")
  (setq python-indent-offset 4))

(use-package snakemake-mode
  :ensure t)

(use-package yaml-mode
  :ensure t)

(use-package indent-guide
  :ensure t
  :hook (python-mode . indent-guide-mode)
  :config
  (setq indent-guide-char "|")
  (setq indent-guide-recursive t))

(evil-leader/set-key-for-mode 'python-mode
  "m v a" 'pyvenv-activate
  "m s b" 'python-shell-send-buffer
  "m s r" 'python-shell-send-region
  "m '" 'python-shell-switch-to-shell)

Scheme/Racket   EXCLUDED

;; Scheme/Racket
;; -------------

;; TODO Work out how to start a repl properly, running the key does
;; not seem to work, I need to run the command via M-x directly.

(require 'racket-mode)
(add-to-list 'auto-mode-alist '("\\.rkt\\'" . racket-mode))
(setq racket-program "/usr/bin/racket")

(evil-leader/set-key-for-mode 'racket-mode
  "m h d" 'racket-describe-search
  "m s b" 'racket-run
  "m s r" 'racket-send-region
  "m s c" 'racket-send-last-sexp)

LaTeX/BibTeX

;; LaTeX/BibTeX
;; ------------

;; TODO Configure this so that there is a good way to search the key
;; bibtex files, perhaps with a SQL type search

;; TODO Find a better way to search BIB files.

(defun most-recent-file (files)
  "Return the most recent file from a list of FILES.
FILES should be a list of file paths as strings."
  (when (and files (seq-every-p #'stringp files))
    (cl-flet* ((file-mod-time (file)
                 (nth 5 (file-attributes file)))
               (mod-time-less-p (a b)
                 (time-less-p (file-mod-time b)
                              (file-mod-time a))))
      (car (sort files #'mod-time-less-p)))))

(defun copy-file-with-bib-extension (file-path)
  "Create a copy of the file at FILE-PATH with a .bib extension."
  (let* ((file-name (file-name-nondirectory file-path))
         (file-base-name (file-name-sans-extension file-name))
         (new-file-name (concat file-base-name ".bib"))
         (new-file-path (concat (file-name-directory file-path) new-file-name)))
    (copy-file file-path new-file-path t)
    new-file-path))

(defun nice-visit-last-bib ()
  "Visit the most recent BIB file in Downloads. If there is a TXT
file that is younger than the last BIB file, send a message to
indicate this."
  (interactive)
  (let* ((bib-files (directory-files "~/Downloads" t ".*bib" "ctime"))
         (most-recent-bib (most-recent-file bib-files))
         (txt-files (directory-files "~/Downloads" t ".*txt" "ctime"))
         (most-recent-txt (most-recent-file txt-files)))
    (if most-recent-bib
        (if (and most-recent-txt
                 (time-less-p (nth 5 (file-attributes most-recent-bib))
                              (nth 5 (file-attributes most-recent-txt))))
            (progn (message (concat "A more recent .txt file exists: " most-recent-txt))
                   (find-file (copy-file-with-bib-extension most-recent-txt)))
          (find-file most-recent-bib))
      (message "No bib files found in ~/Downloads/"))))

(defun nice-ris2bib ()
  "Convert the most recent RIS file in my downloads to a BIB
file. Signal an error if there are no RIS files or if the
conversion fails."
  (interactive "*")

  (let* ((all-ris-files (directory-files "~/Downloads" t ".*ris"))
         (ris-filepath (most-recent-file all-ris-files))
         (target-bib "~/Downloads/new.bib")
         (ris2xml-command (format "ris2xml \"%s\" | xml2bib > %s" ris-filepath
                                  target-bib))
         (command-result (shell-command ris2xml-command)))
    (unless ris-filepath
      (error "No RIS files found in the directory"))
    (unless (zerop command-result)
      (error "Conversion from RIS to BIB failed with error code: %s" command-result))))

(defun nice-bibtex-braces ()
  "Wrap upper case letters with brackets for bibtex titles within
the selected region."
  (interactive)
  (if (use-region-p)
      (let ((start (region-beginning))
            (end (region-end))
            (case-fold-search nil))
        (save-excursion
          (goto-char start)
          (while (re-search-forward "\\([A-Z]+\\)" end t)
            (replace-match (format "{%s}" (match-string 0)) t))))
    (message "No region selected.")))

(defun nice-bibtex-guess-key ()
  "Generate a new key for the current BibTeX entry based on author,
year, and the first two words of the title."
  (interactive)
  (bibtex-beginning-of-entry)
  (let* ((entry (bibtex-parse-entry))
         (author (downcase (replace-regexp-in-string "," "" (car (split-string (bibtex-text-in-field "author"))))))
         (year (bibtex-text-in-field "year"))
         (title (bibtex-text-in-field "title"))
             (first-two-words (when title
                        (let ((split-title (split-string title)))
                          (if (>= (length split-title) 2)
                              (format "%s%s" (nth 0 split-title) (nth 1 split-title))
                            (car split-title))))))
    (if (and author year first-two-words)
        (let ((newkey (format "%s%s%s" author year first-two-words)))
          (kill-new (replace-regexp-in-string "[{}]" "" newkey))
          (evil-jump-item)
          (message "New key generated and copied to clipboard: %s" newkey))
      (error "Author, Year or Title is missing in the current BibTeX entry."))))

(defun nice-browse-url-of-doi ()
  "Open the DOI of the current bibtex entry in the web browser."
  (interactive)
  (save-excursion
    (bibtex-beginning-of-entry)
    (let ((doi (bibtex-autokey-get-field "doi")))
      (if doi
          (browse-url (concat "https://doi.org/" doi))
        (message "No DOI found for this entry")))))

(evil-leader/set-key
  "v b l" 'nice-visit-last-bib
  "v b d" 'nice-browse-url-of-doi
  "v b r" 'nice-ris2bib)

(evil-leader/set-key-for-mode 'bibtex-mode
  "m b b" 'nice-bibtex-braces
  "m b f" 'bibtex-reformat
  "m b k" 'nice-bibtex-guess-key)

In the following we use with-eval-after-load because otherwise the org-latex-classes variable may not have been instantiated.

(with-eval-after-load 'ox-latex
  (add-to-list 'org-latex-classes
               '("scrartcl"
                 "\\documentclass{scrartcl}"
                 ("\\section{%s}" . "\\section*{%s}")
                 ("\\subsection{%s}" . "\\subsection*{%s}")
                 ("\\subsubsection{%s}" . "\\subsubsection*{%s}")
                 ("\\paragraph{%s}" . "\\paragraph*{%s}")
                 ("\\subparagraph{%s}" . "\\subparagraph*{%s}"))))

Markdown

markdown-mode is also useful for writing Rmarkdown, so there are some ESS functions that sneak in here too.

;; Markdown-mode
;; -------------

(use-package markdown-mode
  :mode (("\\.md\\'" . markdown-mode)
         ("\\.Rmd\\'" . markdown-mode))
  :config
  (evil-leader/set-key-for-mode 'markdown-mode
    "m s r" 'ess-eval-region
    "m '" 'ess-switch-to-inferior-or-script-buffer))

Org-mode

  • There is a function org-table-sort-lines which sorts the rows of a table based on a column (1-indexed) with a variety of datatypes acceptable.
;; Org-Mode
;; ========

;; NOTE It would be nice to have an additional command and key for
;; moving from level n+1 headers their parent level n header.

;; FIXME Work out why the configuration based approach does not work!
(setq org-return-follows-link t)
(evil-leader/set-key-for-mode 'org-mode "RET" 'org-open-at-point)

(defun nice-org-mode-hook ()
  "Set up org-mode specific keybindings."
  (local-set-key (kbd "<tab>") #'org-cycle))

(add-hook 'org-mode-hook #'nice-org-mode-hook)

Writing natural language

;; Write well
;; ----------

;; TODO Configure the =dictionary= command so that it works off of a
;; local copy of Webster's

(setq sentence-end-double-space nil)

(use-package flyspell
  :config
  (setq ispell-program-name "aspell")
  (setq ispell-personal-dictionary "~/.aspell.en.pws")
  (set-face-attribute 'flyspell-duplicate nil
                      :underline nil
                      :foreground "white"
                      :background "red")
  (set-face-attribute 'flyspell-incorrect nil
                      :underline nil
                      :foreground "white"
                      :background "red"))

(use-package lorem-ipsum)

(defun nice-diff-dictionaries ()
  "Run ediff on the current ispell-personal-dictionary and the
backup dictionary."
  (interactive)
  (let ((backup-dictionary
         (concat nice-resources-dir "/aspell.en.pws")))
    (ediff-files ispell-personal-dictionary backup-dictionary)))

(evil-leader/set-key
  "t S" 'flyspell-mode ; toggle flyspell on/off.
  "S b" 'flyspell-buffer
  "S n" 'flyspell-goto-next-error
  "S r" 'flyspell-region
  "S c" 'flyspell-correct-word-before-point
  "S d" 'nice-diff-dictionaries)

(use-package writegood-mode)

(setq words-to-add
      '("many" "various" "very" "quite" "somewhat" "several"
        "extremely" "exceedingly" "fairly" "rather" "remarkably" "few"
        "surprisingly" "mostly" "largely" "almost" "nearly" "in which"
        "generally" "virtually" "essentially" "often" "substantially"
        "significantly" "considerably" "typically" "widely" "really"
        "actually" "basically" "certainly" "possibly" "probably"
        "arguably" "likely" "apparently" "clearly" "naturally"
        "obviously" "seemingly" "surely" "somewhat" "allegedly"
        "supposedly" "purportedly" "perhaps" "maybe" "kind of"
        "sort of" "potentially" "ultimately" "respectively"))
(cl-loop for word in words-to-add
         unless (member word writegood-weasel-words)
         do (add-to-list 'writegood-weasel-words word))

(evil-leader/set-key "t w" 'writegood-mode)

;; Formatting text
;; ---------------
;;
;; Some useful functions for writing in natural language.
;;
;; - nice-org-wrapped-lines
;; - nice-org-single-long-line
;; - nice-org-each-sentence-new-line
;;

(defun nice-org-wrapped-lines ()
  "Formats the current paragraph to have wrapped lines at 70"
  (interactive)
  (setq fill-column 70)
  (fill-paragraph)
  (message "Wrapped lines at 70 characters."))

(defun nice-org-single-long-line ()
  "Formats the current paragraph into a single long line."
  (interactive)
  (save-excursion
    (let ((start (progn (backward-paragraph 1) (point)))
          (end (progn (forward-paragraph 1) (point))))
      (goto-char start)
      (while (re-search-forward "[ \t]*\n[ \t]*" end t)
        (replace-match " "))))
  (message "Single long line."))

(defun nice-org-each-sentence-new-line ()
  "Puts each sentence of the current paragraph on a new line."
  (interactive)
  (save-excursion
    (let ((end (save-excursion (forward-paragraph) (point)))
          (beg (save-excursion (backward-paragraph) (point))))
      (goto-char beg)
      (while (< (point) end)
        (forward-sentence)
        ;; Insert newline at the end of a sentence, unless it's the last one.
        (unless (or (= (point) end) (eobp))
          (insert "\n")))))
  (message "Each sentence on a new line."))

LaTeX preview

The following adjusts the size of the latex preview in org-mode. See the binding for org-latex-preview below.

(setq org-format-latex-options (plist-put org-format-latex-options :scale 2.0))

Agenda and calendar (org-mode)

  • I have had some weird warning messages from org-persist about there being difficulty reading some org-mode related data from the cache: "Emacs reader failed to read data in …". I was able to resolve this by closing emacs, deleting the cache files, and then it worked perfectly when I restarted emacs.
  • You can get a list of all available colours to use in keyword faces below with the list-colors-disply command as described in the section below.
;; Org-agenda
;; ----------
;;
;; - `n/p' to move up and down lines.
;; - `v-d' will show the day view.
;; - `v-w' the week view.
;; - `v-m' the month view.
;; - `v-SPC' resets the view.
;; - `.' goes to today.
;; - `j' will /jump/ to a date (selected via calendar).
;; - `t' will cycle through TODO/DONE
;; - `S-<left/right>' moves the scheduled date backwards/forwards
;; - `r' rebuilds the agenda view
;; - `s' in agenda view will save the current org files.
;;
(setq org-agenda-start-day "-14d"
      org-agenda-span 30
      org-agenda-start-on-weekday nil
      org-agenda-start-with-log-mode t
      org-agenda-window-setup 'other-frame
      org-log-done 'time
      org-log-schedule 'time)

(setq org-todo-keywords
      '((sequence "TODO" "DONE")
        (sequence "MEETING" "|" "DONE")
        (sequence "SEMINAR" "|" "DONE")
        (sequence "RESEARCH" "|" "DONE")
        (sequence "ADMIN" "|" "DONE")
        (sequence "DEADLINE" "|" "DONE")
        (sequence "TEACHING" "|" "DONE")
        (sequence "SOCIAL" "|" "DONE")))

(setq org-todo-keyword-faces
      `(("MEETING" . ,(boxed-face "magenta"))
        ("SEMINAR" . ,(boxed-face "magenta"))
        ("RESEARCH" . ,(boxed-face "dark green" "light green"))
        ("DEADLINE" . ,(boxed-face "red" "white"))
        ("ADMIN" . ,(boxed-face "red" "white"))
        ("TEACHING" . ,(boxed-face "magenta"))
        ("SOCIAL" . ,(boxed-face "blue" "#E6ECFF"))))

(defun nice-org-agenda-goto-today-advice-after (&rest _args)
  "Adjust the window after calling `org-agenda-goto-today'."
  (recenter-top-bottom 4))

(advice-add 'org-agenda-goto-today
            :after #'nice-org-agenda-goto-today-advice-after)
(evil-leader/set-key-for-mode 'org-mode "a s" 'org-schedule)
(evil-leader/set-key "a a" 'org-agenda-list)

Agenda and calendar (calfw)   EXCLUDED

;; Calendar view
;;
;; This provides a more classical view of the agenda as a calendar.
;;
(use-package calfw
  :ensure t
  :config
  (use-package calfw-org))

(evil-leader/set-key
  "a a" 'org-agenda-list
  "a c" 'cfw:open-org-calendar)

Literate programming

;; Literate programming

(use-package polymode
  :ensure t
  :mode ("\\.org$" . poly-org-mode)
  :config
  (add-to-list 'auto-mode-alist '("\\.org$" . poly-org-mode)))

(use-package poly-R
  :ensure t
  :after polymode)

(use-package poly-org
  :ensure t
  :after polymode)

(org-babel-do-load-languages
 'org-babel-load-languages
 '((R . t)
   (python . t)))

(evil-leader/set-key-for-mode 'org-mode "b t" 'org-babel-tangle)
(evil-leader/set-key-for-mode 'org-mode "b e" 'org-babel-execute-src-block)

(defun nice-detangle-nicemacs-v2 ()
  "Detangle the nicemacs-v2.el file."
  (interactive)
  (let ((nicemacs-v2-source (concat nice-nicemacs-directory
                                    "/nicemacs-v2.el")))
    (org-babel-detangle nicemacs-v2-source)))

(evil-leader/set-key-for-mode 'emacs-lisp-mode "b d"
  'nice-detangle-nicemacs-v2)

(setq org-image-actual-width 300)
(evil-leader/set-key-for-mode 'org-mode
  "o t l" 'org-latex-preview
  "o t i" 'org-toggle-inline-images)

Website/Publishing

(defun nice-publish-homepage ()
  "Copy my website homepage if it exists."
  (interactive)
  (let* ((notes-root "~/public-site/org/")
         (misc-root "~/public-site/org/misc/papers/")
         (local-notes (concat notes-root "index-notes.html"))
         (remote-notes (concat nice-website-directory "notes.html"))
         (local-mininyan (concat notes-root "mininyan.js"))
         (remote-mininyan (concat nice-website-directory "mininyan.js"))
         (local-landing (concat notes-root "index-academic.html"))
         (remote-landing (concat nice-website-directory "index.html"))
         (local-css (concat notes-root "microgram.css"))
         (remote-css (concat nice-website-directory "microgram.css"))
         (local-misc-index (concat misc-root "index.html"))
         (remote-misc-index (concat nice-website-directory "misc/papers/index.html"))
         (local-misc-data (concat misc-root "data.json"))
         (remote-misc-data (concat nice-website-directory "misc/papers/data.json"))
         (local-misc-script (concat misc-root "script.js"))
         (remote-misc-script (concat nice-website-directory "misc/papers/script.js")))
    (when (file-exists-p local-notes)
      (copy-file local-notes remote-notes t)
      (message "Copied %s to %s" local-notes remote-notes))
    (when (file-exists-p local-mininyan)
      (copy-file local-mininyan remote-mininyan t)
      (message "Copied %s to %s" local-mininyan remote-mininyan))
    (when (file-exists-p local-landing)
      (copy-file local-landing remote-landing t)
      (message "Copied %s to %s" local-landing remote-landing))
    (when (file-exists-p local-css)
      (copy-file local-css remote-css t)
      (message "Copied %s to %s" local-css remote-css))
    (when (file-exists-p local-misc-index)
      (copy-file local-misc-index remote-misc-index t)
      (message "Copied %s to %s" local-misc-index remote-misc-index))
    (when (file-exists-p local-misc-data)
      (copy-file local-misc-data remote-misc-data t)
      (message "Copied %s to %s" local-misc-data remote-misc-data))
    (when (file-exists-p local-misc-script)
      (copy-file local-misc-script remote-misc-script t)
      (message "Copied %s to %s" local-misc-script remote-misc-script))))

;; The following projects are available for publishing when the
;; `org-publish' command is given.
(setq org-publish-project-alist
      `(("website-notes-org-files"
         :base-directory "~/public-site/org/notes/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/notes/"
         :publishing-function org-html-publish-to-html)
        ("website-teaching-org-files"
         :base-directory "~/public-site/org/teaching/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/teaching/"
         :publishing-function org-html-publish-to-html)
        ("website-teaching-static"
         :base-directory "~/public-site/org/teaching/"
         :base-extension "css\\|pdf"
         :publishing-directory "~/aezarebski.github.io/teaching/"
         :recursive t
         :publishing-function org-publish-attachment)
        ("website-lists-org-files"
         :base-directory "~/public-site/org/lists/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/lists/"
         :publishing-function org-html-publish-to-html)
        ("website-images-static"
         :base-directory "~/public-site/org/images/"
         :base-extension "png"
         :publishing-directory "~/aezarebski.github.io/images/"
         :publishing-function org-publish-attachment)
        ("website-misc-ggplot2-org-files"
         :base-directory "~/public-site/org/misc/ggplot2/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/misc/ggplot2/"
         :publishing-function org-html-publish-to-html)
        ("website-misc-ggplot2-static"
         :base-directory "~/public-site/org/misc/ggplot2/"
         :base-extension "png\\|jpg\\|pdf"
         :publishing-directory "~/aezarebski.github.io/misc/ggplot2/"
         :publishing-function org-publish-attachment)
        ("website-misc-basegraphicsR-org-files"
         :base-directory "~/public-site/org/misc/basegraphicsR/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/misc/basegraphicsR/"
         :publishing-function org-html-publish-to-html)
        ("website-misc-basegraphicsR-static"
         :base-directory "~/public-site/org/misc/basegraphicsR/"
         :base-extension "png\\|jpg\\|pdf"
         :publishing-directory "~/aezarebski.github.io/misc/basegraphicsR/"
         :publishing-function org-publish-attachment)
        ("website-misc-latex-org-files"
         :base-directory "~/public-site/org/misc/latex/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/misc/latex/"
         :publishing-function org-html-publish-to-html)
        ("website-misc-latex-static"
         :base-directory "~/public-site/org/misc/latex/"
         :base-extension "png\\|jpg\\|pdf"
         :publishing-directory "~/aezarebski.github.io/misc/latex/"
         :publishing-function org-publish-attachment)
        ("website-misc-tikz-org-files"
         :base-directory "~/public-site/org/misc/tikz/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/misc/tikz/"
         :publishing-function org-html-publish-to-html)
        ("website-misc-tikz-static"
         :base-directory "~/public-site/org/misc/tikz/"
         :base-extension "png\\|jpg\\|pdf"
         :publishing-directory "~/aezarebski.github.io/misc/tikz/"
         :publishing-function org-publish-attachment)
        ("website-misc-matplotlib-org-files"
         :base-directory "~/public-site/org/misc/matplotlib/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/misc/matplotlib/"
         :publishing-function org-html-publish-to-html)
        ("website-misc-matplotlib-static"
         :base-directory "~/public-site/org/misc/matplotlib/"
         :base-extension "png\\|jpg\\|pdf"
         :publishing-directory "~/aezarebski.github.io/misc/matplotlib/"
         :publishing-function org-publish-attachment)
        ("website-misc-ml-org-files"
         :base-directory "~/public-site/org/misc/ml/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/misc/ml/"
         :publishing-function org-html-publish-to-html)
        ("website-misc-ml-static"
         :base-directory "~/public-site/org/misc/ml/"
         :base-extension "webp\\|png\\|py"
         :recursive t
         :publishing-directory "~/aezarebski.github.io/misc/ml/"
         :publishing-function org-publish-attachment
         :exclude "venv/")
        ("website-misc-ml-diagrams-static"
         :base-directory "~/public-site/org/misc/ml/diagrams/"
         :base-extension "png"
         :publishing-directory "~/aezarebski.github.io/misc/ml/diagrams/"
         :publishing-function org-publish-attachment)
        ("website-misc-plotnine-org-files"
         :base-directory "~/public-site/org/misc/plotnine/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/misc/plotnine/"
         :publishing-function org-html-publish-to-html)
        ("website-misc-plotnine-static"
         :base-directory "~/public-site/org/misc/plotnine/"
         :base-extension "png\\|jpg\\|pdf"
         :publishing-directory "~/aezarebski.github.io/misc/plotnine/"
         :publishing-function org-publish-attachment)
        ("website-misc-recipes"
         :base-directory "~/public-site/org/misc/recipes/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/misc/recipes/"
         :publishing-function org-html-publish-to-html)
        ("website-misc-recipes-static"
         :base-directory "~/public-site/org/misc/recipes/"
         :base-extension "png\\|css"
         :publishing-directory "~/aezarebski.github.io/misc/recipes/"
         :recursive ()
         :publishing-function org-publish-attachment)
        ("review2-org"
         :base-directory "~/Documents/bibliography/review2"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/notes/review2"
         :recursive ()
         :publishing-function org-html-publish-to-html
         )
        ("review2-static"
         :base-directory "~/Documents/bibliography/review2"
         :base-extension "css\\|png"
         :publishing-directory "~/aezarebski.github.io/notes/review2"
         :recursive t
         :publishing-function org-publish-attachment
         )
        ("nicemacs-org-files"
         :base-directory "~/Documents/nicemacs/"
         :base-extension "org"
         :publishing-directory "~/aezarebski.github.io/misc/nicemacs/"
         :publishing-function org-html-publish-to-html)
        ("python"
         :components ("website-misc-matplotlib-org-files"
                      "website-misc-matplotlib-static"
                      "website-misc-plotnine-org-files"
                      "website-misc-plotnine-static"))
        ("ml"
         :components ("website-misc-ml-org-files"
                      "website-misc-ml-static"
                      "website-misc-ml-diagrams-static"))
        ("R"
         :components ("website-misc-basegraphicsR-org-files"
                      "website-misc-basegraphicsR-static"
                      "website-misc-ggplot2-org-files"
                      "website-misc-ggplot2-static"))
        ("review"
         :components ("review2-org"
                      "review2-static"))
        ("latex"
         :components ("website-misc-latex-org-files"
                      "website-misc-latex-static"
                      "website-misc-tikz-org-files"
                      "website-misc-tikz-static"))
        ("recipes"
         :components ("website-misc-recipes"
                      "website-misc-recipes-static"))
        ("teaching"
         :components ("website-teaching-org-files"
                      "website-teaching-static"))
        ("website"
         :components ("website-notes-org-files"
                      "website-images-static"
                      "website-lists-org-files"
                      "ml"
                      "nicemacs-org-files"
                      "recipes"
                      "review"
                      "latex"
                      "python"
                      "R"))))

STUFF 8

;; Visitors
;; ========

(defmacro NVNF (fname pname file key)
  "Macro to define a function for visiting a notes file and set an Evil leader key binding.

  This macro takes in four arguments:
  - FNAME: A string that will be used to construct the function name.
  - PNAME: A string that will be used in the message displayed to the user.
  - FILE: A string that represents the name of the notes file.
  - KEY: A string that represents the keybinding for the function using the Evil leader.

  The function created by this macro opens the notes file specified by FILE in
  the directory specified by `nice-notes-directory'. The keybinding is set using
  the Evil leader, and is constructed using the specified KEY string.

  Example usage:
  (NVNF \"my-notes\" \"My Notes\" \"my-notes.org\" \"n\")"

  `(progn
     (defun ,(intern (format "nice-visit-%s" fname)) ()
       "Visit a notes file."
       (interactive)
       (progn
         (message ,(format "Visiting %s" pname))
         (find-file ,(concat nice-notes-directory "/" file))))
     (evil-leader/set-key ,(concat "v n " key) (intern ,(format "nice-visit-%s" fname)))))

(defmacro NVF (fname pname file key)
  `(progn
     (defun ,(intern (format "nice-visit-%s" fname)) ()
       "Visit a file."
       (interactive)
       (progn
         (message ,(format "Visiting %s" pname))
         (find-file ,file)))
     (evil-leader/set-key ,(concat "v f" key) (intern ,(format "nice-visit-%s" fname)))))

(defmacro NVD (dname pname path key)
  "Macro to define a function for visiting a directory and set an Evil leader key binding.

  This macro takes in four arguments:
  - DNAME: A string that will be used to construct the function name.
  - PNAME: A string that will be used in the message displayed to the user.
  - PATH: A string that represents the path of the directory.
  - KEY: A string that represents the keybinding for the function using the Evil leader.

  The function created by this macro jumps to the directory specified by PATH using `dired-jump'.
  The keybinding is set using the Evil leader, and is constructed using the specified KEY string.

  Example usage:
  (NVD \"my-dir\" \"My Directory\" \"/path/to/directory\" \"d\")"

  `(progn
     (defun ,(intern (format "nice-visit-%s" dname)) ()
       "Visit a directory."
       (interactive)
       (progn
         (message ,(format "Visiting %s" pname))
         (dired-jump nil ,path)
         (revert-buffer)))
     (evil-leader/set-key ,(concat "v d " key) (intern ,(format "nice-visit-%s" dname)))))

(NVF nicemacs2-source "Nicemacs v2 source" "~/Documents/nicemacs/nicemacs-v2.el" "e 3")
(NVF nicemacs2-init "Nicemacs v2 init.el" "~/.emacs.d/init.el" "e 2")
(NVF nicemacs-init "Nicemacs v1 nicemacs.el" "~/Documents/nicemacs/nicemacs.el" "e 1")
(NVF nicemacs-org "Nicemacs v1 nicemacs.org" "~/Documents/nicemacs/nicemacs.org" "e 1")
(NVF review-2 "Review 2" "~/Documents/bibliography/review2/review.org" "r 2")
(NVF review-reading-list "Reading list" "~/Documents/bibliography/review2/reading-list.org" "r l")
(NVF review-references "Bibtex references" "~/Documents/bibliography/references.bib" "r r")

(NVNF academia-notes "Academia notes" "academic-journal-notes.org" "a")
(NVNF beast-notes "BEAST2 notes" "beast2-notes.org" "b")
(NVNF git-notes "Git notes" "git-notes.org" "g")
(NVNF haskell-notes "Haskell notes" "haskell-notes.org" "h")
(NVNF java-notes "Java notes" "java-notes.org" "j")
(NVNF latex-notes "LaTeX notes" "latex-notes.org" "l")
(NVNF mathematica-notes "Mathematica notes" "mathematica-notes.org" "m")
(NVNF org-mode-notes "org-mode notes" "org-mode-notes.org" "o")
(NVNF python-notes "Python notes" "python-notes.org" "p")
(NVNF r-notes "R notes" "r-notes.org" "r")
(NVNF ubuntu-notes "Ubuntu/Linux notes" "linux-notes.org" "u")

(NVD emacs "Emacs" "~/.emacs.d/fake.org" "e")
(NVD journal-dir "Journal Directory" "~/Documents/journal/fake.org" "j")
(NVD library "Library" "~/Documents/library/fake.org" "l")
(NVD manuscripts "Manuscripts" "~/Documents/manuscripts/fake.org" "m")
(NVD music "Music" "~/Music/fake.org" "M")
(NVD documents "Documents" "~/Documents/fake.org" "d")
(NVD downloads "Downloads" "~/Downloads/fake.org" "D")
(NVD professional "Professional" "~/Documents/professional/README.org" "p")
(NVD projects "Projects" "~/projects/fake.org" "P")
(NVD teaching "Teaching" "~/Documents/teaching/fake.org" "t")
(NVD website-org "Website (org files)" "~/public-site/org/fake.org" "w")
(NVD website-html "Website (HTML files)" "~/aezarebski.github.io/fake.org" "W")
(NVD notes "My notes" "~/public-site/org/notes/fake.org" "n")
(NVD yasnippet "Yasnippet" "~/.emacs.d/snippets/fake.org" "y")

(setq org-agenda-files
      (list (concat nice-journal-directory "bike.org")))

(defun nice-visit-journal ()
  "Opens the current journal file. If it does not yet exist, it
  makes a copy of the one from one week ago. This will also
  ensure that the current journal file is among the org agenda
  files and that a previous one is not."
  (interactive)
  (let* ((filepath-template (concat nice-journal-directory "journal-%s.org"))
         (curr-file (format filepath-template (format-time-string "%Y-%m")))
         (prev-file (format filepath-template (format-time-string "%Y-%m" (time-subtract (current-time) (* 7 24 60 60))))))
    (unless (file-exists-p curr-file)
      (message "Creating new journal file")
      (copy-file prev-file curr-file))
    (message "Opening journal file")
    (when (member prev-file org-agenda-files)
      (setq org-agenda-files (remove prev-file org-agenda-files)))
    (unless (member curr-file org-agenda-files)
      (add-to-list 'org-agenda-files curr-file))
    (find-file curr-file)
    (goto-char (point-min))
    (recenter-top-bottom)))

(evil-leader/set-key "v f j" 'nice-visit-journal)

Copilot

;; Copilot
;; =======
;;
;; To install this you need to clone the repository and a couple of
;; dependencies yourself: s, editorconfig which are emacs packages and
;; node.js.
;;
;; To enable `copilot' on your buffer, use SPC t c.
;;
(use-package copilot
  :defer 1
  :config
  (evil-leader/set-key "t c" 'copilot-mode)
  (setq copilot-node-executable "~/.nvm/versions/node/v17.3.1/bin/node")
  ;; (setq copilot-node-executable "/usr/bin/node")
  :load-path "~/.emacs.d/copilot.el/")

(defun nice-copilot-tab ()
  "Accept the current suggestion provided by copilot."
  (interactive)
  (or (copilot-accept-completion)
      (indent-for-tab-command)))

(with-eval-after-load 'copilot
  (evil-define-key 'insert copilot-mode-map
    (kbd "<tab>") #'nice-copilot-tab))

(defun nice-copilot-by-line ()
  "Accept the current suggestion by line."
  (interactive)
  (or (copilot-accept-completion-by-line)
      (indent-for-tab-command)))

(with-eval-after-load 'copilot
  (evil-define-key 'insert copilot-mode-map
    (kbd "C-<tab>") #'nice-copilot-by-line))

Rust

(use-package rust-mode
:ensure t
:mode "\\.rs\\'"
:config
;; Enable rustfmt on save
(setq rust-format-on-save t))

STUFF 9

;; Explore new worlds
;; ==================

;; TODO Work out how to browse gopher with =gopher.el=.

;; TODO Work out how to configure auth-source.

;; TODO Work out how to use mediawiki-mode to read and edit wikipedia.

;; TODO Explore running spotify through emacs

;; Customization
;; =============

;; There be dragons here
;; ---------------------

GNU Emacs

The notes here are intended to deal exclusively with GNU emacs without the use of packages other than those that are provided with emacs.

Build you an Emacs

Get the source code from here with

# wget https://git.savannah.gnu.org/cgit/emacs.git/snapshot/emacs-VERSION.tar.gz
wget https://ftp.gnu.org/gnu/emacs/emacs-VERSION.tar.xz
tar -xf emacs-VERSION.tar.xz

Alternatively, you can get clone the emacs mirror from GitHub and check out the emacs-28 branch (or whatever version you want).

Follow the instructions in the INSTALL file to build emacs.

  • This seems to just be ./configure then make then sudo make install.
    • ./configure --with-native-compilation --with-rsvg.
    • If you cannot find the configure script, you may need to run autogen.sh first.
  • If you have spare compute you can use multiple jobs to speed up the compilation with make -j[N] to use N jobs during compilation

Notes

  • emacs-29.4: same procedure as the previous version.
  • emacs-29.2: same procedure as the previous version.
  • emacs-29.1 on the work laptop: ./configure --with-json --with-rsvg --with-native-compilation --with-imagemagick CFLAGS'-g3 -O3'=
    • When building from source on a completely fresh system I needed a lot of basic packages: build-essential autoconf texinfo libgnutls28-dev libjansson-dev
  • emacs-29.0.60 on a new laptop: ./configure --with-native-compilation --with-tree-sitter --without-x --with-pgtk because it uses Wayland.
  • emacs-29.0.60 configured with --with-native-compilation, and --with-tree-sitter . This took a bit of fiddling because it couldn't find the correct version of the JIT library which turned out to be libgccjit-11-dev and I couldn't work out how to compile tree-sitter from source.
  • emacs-28.2
  • emacs-28.1.90 configured with --with-native-compilation and --with-rsvg. Building this one seemed to take longer than normal.
  • emacs-28.1. I also installed libgccjit and used ./configure --with-native-compilation during the compilation, it does feel snappier.
  • emacs-28.0.91 requested mailutils to be installed during configuration.
  • emacs-28.0.60 requested libacl1-dev and libharfbuzz-dev be installed during configuration. It does feel snappier. It told me that my current version of GTK+ leads to a bug but I couldn't figure out how to update GTK+ and it seemed to be an up to date version anyway.
  • emacs-27.2 build and installs without issue.

Recording keyboard macros

  1. Start recording with C-x ( (which calls kmacro-start-macro).
  2. Stop recording with C-x ) (which calls kmacro-end-macro).
  3. Execute the recording with C-x e (which calls kmacro-end-and-call-macro).

If you want to save a macro for later use, you can get a emacs-lisp definition of it with insert-kbd-macro.

Buffer specific variables

Suppose you wanted to set the fill-column for a specific file, add the following to the end of the file to set it to 80 for this file.

% Local Variables:
% fill-column: 80
% End:

Colours

The function list-colors-display will open a new buffer displaying all the defined colours and their names. This is particularly useful if you want to configure faces.

Jargon

There is a glossary in the manuals, the nodes are Emacs > Glossary. The regex search entered with s is very useful here.

TRAMP

TRAMP stands for "Transparent Remote (file) Access, Multiple Protocol". In general, TRAMP connections use the following syntax: /protocol:[user@]hostname[#port]:/path/to/file. If you usually SSH into server and you want to edit files there then the protocol will be ssh. If you omit the protocol it will use the method named by the variable tramp-defaul-method. (Mickey Petersen recommends setting tramp-default-method to ssh for convenience.) If you set up an ~/.ssh/config file, TRAMP will be aware of these hosts. Once you are visiting a file in this way, the standard emacs functionality will work so ou can treat it like any other buffer.

Example

Suppose you had the ssh configuration shown below:

Host foobar
     HostName squanch.beep.boop
     User flappy

Then you could run find-file with the following to access a file at ~/bing/bong/readme.org:

/ssh:foobar:~/bing/bong/readme.org

And amazingly, tab-complete of paths should also work! If you give the path to a directory, this will open in dired.

Mastering Emacs

Here are some notes from reading Mastering Emacs.

Chapter 2

  • "In Emacs, the buffer is the data structure."
  • A window is a tiled portion of a frame.
  • The modeline is the portion at the bottom of a window that displays information such as the name of the buffer displayed and the major mode.
  • The minibuffer is the below the modeline and displays messages.
  • The point is the current position of the cursor.
  • The region is a selection of text which has the point at one end and the mark at the other. The region is visually displayed with the transient mark mode (TMM).
  • killing is cutting text, yanking is pasting it, and saving to the kill ring is copying.
  • font locking is syntax highlighting.

Chapter 3

  • In order for a function to be executed by M-x, it needs to be made interactive.
  • apropos is a system to for discovery:
    • apropos searches everything,
    • apropos-command searches commands,
    • and apropos-documentation searches documentation.
  • The describe system is a collection of functions that allow you to obtain information about known items:
    • describe-mode,
    • describe-function,
    • describe-variable,
    • and describe-key.

Chapter 6

  • There is the function read-only-mode which toggles read only mode, which replaces the obsolete toggle-read-only function.

Getting HELP

There are a couple of help menus that are useful to be able to access easily:

  • GNU Emacs NEWS can be summoned with view-emacs-news.
  • Spacemacs documentation can be summoned with helm-spacemacs-help-docs.
  • GNU Emacs Manual can be summoned with info-display-manual.
  • Emacs Lisp Intro has a section on debugging.

Info navigation

The following are key-bindings for emacs mode (use \ to call evil-execute-in-emacs-state):

  • n next node
  • p previous node
  • ^ will move up
  • RET will follow a link
  • l return to the last node visited
  • s search with a regex
  • f find a node linked from here
  • d go to the root node

Author: Alexander E. Zarebski

Created: 2024-10-30 Wed 14:50

Validate