My Emacs Configuration

This is my emacs configuration, as typed in org-babel. Its a form of literate programming which is really nice in that it basically goads me into writing a better documented emacs config.

Pre-install

To set up the relevant binaries, it's expected that nix is installed and configured.

(setq async-shell-command-buffer 'new-buffer) ;; don't prompt for processes opening buffers.

TODOs

DONE Migrate to .config/emacs/ rather than .emacs.d/

TODO setup a sql format tool

TODO look into these packages

Initial setup

Increase the garbage collection threshold so we don't spend a bunch of time in startup doing GC. We'll turn it back lower (so we do small quick ones) after we're done loading.

;; The default is 800 kilobytes.  Measured in bytes.
;; without: *** Emacs loaded in 21.20 seconds with 55 garbage collections.
;; with   : *** Emacs loaded in 19.34 seconds with 18 garbage collections.
(setq gc-cons-threshold (* 100 1000 1000))

;; Profile emacs startup
(add-hook 'emacs-startup-hook
          (lambda ()
            (message "*** Emacs loaded in %s with %d garbage collections."
                     (format "%.2f seconds"
                             (float-time
                              (time-subtract after-init-time before-init-time)))
                     gcs-done)))

Having Ubuntu's Unity intercept my superkey is less than ideal. unity-tweak-tool makes that work for me. This comment on stack exchange says I have to click Unity > Additional. From there, I uncheck the "Hold Super for keyboard shortcuts" option, disable "Invoke HUD" (clicking they keybinding and pressing backspace), then make the "Show the Launcher" option <Super>space.

Setup package manager

Get a bootstrapped version of straight.el.

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

And make it look like package.el while we're at it.

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

And tell it about melpa, so we can get access to more packages.

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

To get our paths setup correctly (for test running, etc), pull our exec-path out of our shell settings.

(if (not (equal window-system 'w32))
    (progn
      (use-package exec-path-from-shell)
      (exec-path-from-shell-initialize)))

Some packages require specific emacs versions, so provide a helper function for later.

(defun emacs-version-gt-p (at-least-major at-least-minor)
  (let* ((version-arr (split-string emacs-version "\\."))
         (major (string-to-number (nth 0 version-arr)))
         (minor (string-to-number (nth 1 version-arr))))
    (if (> major at-least-major)
        t
      (if (and (equal major at-least-major) (>= minor at-least-minor))
          t
        nil))))

Look & Feel

I've taken the bulk of these from the elegant-emacs repository, which makes things look much simpler.

(defun font-existsp (font)
  (if (string-equal (describe-font font)
                    "No matching font found")
      nil
    t))

(defun abrahms/set-font (font face)
  (if (font-existsp font)
      (set-face-attribute face nil
                            :font font
                            :weight 'light
                            :height 120
                            )
      (message (concat "Missing font: " font)))
  )


;; separated out so, in the event of an error, it's clearer which one broke.
(abrahms/set-font "ProFont IIx Nerd Font Mono" 'default)
(abrahms/set-font "ProFont IIx Nerd Font Mono" 'fixed-pitch)
(abrahms/set-font "Vollkorn" 'variable-pitch)

(if (display-graphic-p)
    (progn
      (setq use-dialog-box nil) ;; no popups
      (menu-bar-mode -1) ;; minimal chrome
      (tool-bar-mode -1) ;; no toolbar
      (scroll-bar-mode -1))) ;; disable scroll bars

(set-frame-parameter (selected-frame)
                     'internal-border-width 10)
(set-frame-parameter (selected-frame) 'alpha '(95 . 75))
(setq default-frame-alist
      (append (list '(width  . 80) '(height . 40)
                    '(vertical-scroll-bars . nil)
                    '(internal-border-width . 24))))


;; Line spacing, can be 0 for code and 1 or 2 for text
(setq-default line-spacing 0)

;; Underline line at descent position, not baseline position
(setq x-underline-at-descent-line t)

;; No ugly button for checkboxes
(setq widget-image-enable nil)

;; Line cursor and no blink
(set-default 'cursor-type  '(bar . 3))
(blink-cursor-mode 0)

;; No sound
(setq visible-bell t)
(setq ring-bell-function 'ignore)

;; Paren mode is part of the theme
(show-paren-mode t)

;; No fringe but nice glyphs for truncated and wrapped lines

(if (display-graphic-p)
    (fringe-mode '(0 . 0)))
(defface fallback '((t :family "Fira Code Light")) "Fallback")
;; (set-display-table-slot standard-display-table 'truncation
;;                         (make-glyph-code ?… 'fallback))
;; (set-display-table-slot standard-display-table 'wrap
;;                         (make-glyph-code ?↩ 'fallback))

;; simplified mode line
(defun mode-line-render (left right)
  (let* ((available-width (- (window-width) (length left) )))
    (format (format "%%s %%%ds" available-width) left right)))
(setq-default mode-line-format
              '((:eval
                 (mode-line-render
                  (format-mode-line (list
                                     (propertize "☰" 'face `(:inherit mode-line-buffer-id)
                                                 'help-echo "Mode(s) menu"
                                                 'mouse-face 'mode-line-highlight
                                                 'local-map   mode-line-major-mode-keymap)
                                     " %b "))
                  (format-mode-line "%4l:%2c  ")))))

;; move modeline to the top of the buffer
;; Not sure if I like this yet..
(setq-default header-line-format mode-line-format)
(setq-default mode-line-format nil)
(setq display-time-format "%l:%M %p %b %y")
(setq display-time-default-load-average nil)


;; Vertical window divider
(setq window-divider-default-right-width 3)
(setq window-divider-default-places 'right-only)
(window-divider-mode)

Setup diminish, which reduces the clutter in the modeline.

(use-package diminish)

Themes

Doom-themes are a modern set of themes for emacs.

(use-package doom-themes
  :config
  ;; Global settings (defaults)
  (setq doom-themes-enable-bold t    ; if nil, bold is universally disabled
        doom-themes-enable-italic t) ; if nil, italics is universally disabled

  ;; Enable flashing mode-line on errors
  (doom-themes-visual-bell-config)

  ;; Enable custom neotree theme (all-the-icons must be installed!)
  (doom-themes-neotree-config)
  ;; or for treemacs users
  (setq doom-themes-treemacs-theme "doom-colors") ; use the colorful treemacs theme
  (doom-themes-treemacs-config)

  ;; Corrects (and improves) org-mode's native fontification.
  (doom-themes-org-config))

(use-package plan9-theme)

(load-theme 'doom-monokai-pro t)

Emoji

Having emoji is nice.

(use-package emojify
  :hook (org-mode . emojify-mode)
  :commands emojify-mode)

Ivy

Ivy is an improvement to emacs's extended-command-running functionality. It changes the menu to provide fuzzy match auto-completion stuff, which is speedier than helm and more compact.

; Let ivy use flx for fuzzy-matching
;; (require 'flx)
;; (setq ivy-re-builders-alist '((t . ivy--regex-fuzzy)))

(use-package ivy
  :diminish
  :bind (:map ivy-minibuffer-map
         ("C-m" . ivy-alt-done))
  :init
  (ivy-mode 1)
  :config
  (setq ivy-use-virtual-buffers t)
  (setq ivy-wrap t)
  (setq ivy-count-format "(%d/%d) ")
  (setq enable-recursive-minibuffers t)
  (setq projectile-completion-system 'ivy) ; Let projectile use ivy

  ;; Use different regex strategies per completion command
  (push '(completion-at-point . ivy--regex-fuzzy) ivy-re-builders-alist) ;; This doesn't seem to work...
  (push '(counsel-M-x . ivy--regex-ignore-order) ivy-re-builders-alist)

  ;; Set minibuffer height for different commands
  (setf (alist-get 'counsel-projectile-ag ivy-height-alist) 15)
  (setf (alist-get 'counsel-projectile-rg ivy-height-alist) 15)
  (setf (alist-get 'counsel-switch-buffer ivy-height-alist) 7))

(use-package ivy-hydra
  :defer t
  :after hydra)

(use-package ivy-rich
  :init
  (ivy-rich-mode 1)
  :after counsel
  :config
  (setq ivy-format-function #'ivy-format-function-line)
  (setq ivy-rich-display-transformers-list
        (plist-put ivy-rich-display-transformers-list
                   'ivy-switch-buffer
                   '(:columns
                     ((ivy-rich-candidate (:width 40))
                      (ivy-rich-switch-buffer-indicators (:width 4 :face error :align right)); return the buffer indicators
                      (ivy-rich-switch-buffer-major-mode (:width 12 :face warning))          ; return the major mode info
                      (ivy-rich-switch-buffer-project (:width 15 :face success))             ; return project name using `projectile'
                      (ivy-rich-switch-buffer-path (:width (lambda (x) (ivy-rich-switch-buffer-shorten-path x (ivy-rich-minibuffer-width 0.3))))))  ; return file path relative to project root or `default-directory' if project is nil
                     :predicate
                     (lambda (cand)
                       (if-let ((buffer (get-buffer cand)))
                           ;; Don't mess with EXWM buffers
                           (with-current-buffer buffer
                             (not (derived-mode-p 'exwm-mode)))))))))

(use-package flx  ;; Improves sorting for fuzzy-matched results
  :after ivy
  :defer t
  :init
  (setq ivy-flx-limit 10000)
  (setq ivy-re-builders-alist '((t . ivy--regex-fuzzy))))


;; (use-package ivy-posframe

;;   ;; previously:
;;   ;;   (setq ivy-posframe-display-functions-alist '((t . ivy-posframe-display-at-frame-top-center))
;;   ;;       ivy-posframe-height-alist '((t . 20))
;;   ;;       ivy-posframe-parameters '((internal-border-width . 10)))

;;   :custom
;;   (ivy-posframe-width      115)
;;   (ivy-posframe-min-width  115)
;;   (ivy-posframe-height     10)
;;   (ivy-posframe-min-height 10)
;;   :config
;;   (setq ivy-posframe-display-functions-alist '((t . ivy-posframe-display-at-frame-top-center)))
;;   (setq ivy-posframe-parameters '((parent-frame . nil)
;;                                   (left-fringe . 8)
;;                                   (right-fringe . 8)))
;;   (ivy-posframe-mode 1))

(use-package prescient
  :after counsel
  :config
  (prescient-persist-mode 1))

(use-package ivy-prescient
  :after prescient
  :config
  (ivy-prescient-mode 1))

Fill Column Indicator

Fill column indicator will show you the current fill-column as a vertical line in your buffers. This is helpful for making sure your code doesn't go over 80 characters wide for things like python.

(use-package fill-column-indicator) ;; line indicating some edge column

Writing

Org-Mode

org-mode itself

(defun abrahms/org-mode-setup ()
  (variable-pitch-mode 1)
  (auto-fill-mode 0)
  (visual-line-mode 1)
  (diminish org-indent-mode))

(use-package org
  :bind (("\C-ca" . org-agenda)
         :map org-mode-map
         ("C-c l" . org-store-link)
         ("C-c s" . ja/windows-screenshot))

  :hook ((org-mode . abrahms/org-mode-setup))

  :config
  (setq org-ellipsis " ▾"
        org-hide-emphasis-markers t
        org-image-actual-width '(550)
        org-src-fontify-natively t
        org-enforce-todo-dependencies t
        org-fontify-quote-and-verse-blocks t
        org-src-tab-acts-natively t
        org-edit-src-content-indentation 2
        org-hide-block-startup nil

When compiling code (graphviz graphs, etc) with org-babel, I don't want to see prompts of "are you sure you want to do this?" for each code block, so we'll squelch them.

org-confirm-babel-evaluate nil

Setup my org-mode keywords to include waiting & blocked:

org-todo-keywords
              '((sequence "TODO" "NEXT" "WAITING" "BLOCKED" "DONE"))
        org-src-preserve-indentation t
        org-startup-folded 'content
        org-cycle-separator-lines 2
        org-modules nil
        org-directory (cond
                             ;; it's in the roaming data
                             ((eq system-type 'windows-nt) "\\..\\..\\docs\\")
                             ;; done via syncthing
                             ((eq system-type 'darwin) (concat (getenv "HOME") "/docs/"))
                             ;; use dropbox on home computer
                             (t (concat (getenv "HOME") "/Dropbox/docs/")))
         org-agenda-files (directory-files-recursively org-directory "org$")
         org-refile-targets '((nil :maxlevel . 9)
                                        (org-agenda-files :maxlevel . 9))
         org-outline-path-complete-in-steps nil         ; Refile in a single go
         org-refile-use-outline-path t                  ; Show full paths for refiling
         org-log-done 'note

        )

  (set-face-attribute 'org-document-title nil :font "Vollkorn" :weight 'bold :height 1.3)
  (dolist (face '((org-level-1 . 1.2)
                  (org-level-2 . 1.1)
                  (org-level-3 . 1.05)
                  (org-level-4 . 1.0)
                  (org-level-5 . 1.1)
                  (org-level-6 . 1.1)
                  (org-level-7 . 1.1)
                  (org-level-8 . 1.1)))
    (set-face-attribute (car face) nil :font "Vollkorn" :weight 'medium :height (cdr face)))



;; Make sure org-indent face is available
(require 'org-indent)

;; Ensure that anything that should be fixed-pitch in Org files appears that way
(set-face-attribute 'org-block nil :foreground nil :inherit 'fixed-pitch)
(set-face-attribute 'org-table nil  :inherit 'fixed-pitch)
(set-face-attribute 'org-formula nil  :inherit 'fixed-pitch)
(set-face-attribute 'org-code nil   :inherit '(shadow fixed-pitch))
(set-face-attribute 'org-indent nil :inherit '(org-hide fixed-pitch))
(set-face-attribute 'org-verbatim nil :inherit '(shadow fixed-pitch))
(set-face-attribute 'org-special-keyword nil :inherit '(font-lock-comment-face fixed-pitch))
(set-face-attribute 'org-meta-line nil :inherit '(font-lock-comment-face fixed-pitch))
(set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch)

;; Get rid of the background on column views
(set-face-attribute 'org-column nil :background nil)
(set-face-attribute 'org-column-title nil :background nil)
;; maybe https://github.com/alphapapa/magit-todos ?
) ; close opening from org block.

There are useful things in the contrib repo for org as well.

(use-package org-contrib)

When using excorporate, I found that the agenda sometimes pulled in things it shouldn't. This will ensure that we don't parse datetimes wrong.

(use-package diary-lib
  :config
  (setq original-diary-time-regexp diary-time-regexp)
  (setq diary-time-regexp (concat "\\b" diary-time-regexp)))

I export things to xwiki sometimes. The mediawiki plugin mostly works, but links are wrong. This advice fixes it.

(defun xwiki-link-syntax (orig-fun &rest args)
  "Turns [link text] into [[text>>link]] to satisfy xwiki link formatting"
  (let ((res (apply orig-fun args)))
    (save-match-data
      (if (string-match "\\[\\(.*\\) \\(.*\\)\\]" res)
          (let ((link (match-string 1 res))
                (text (match-string 2 res)))
            (format "[[%s>>%s]]" text link))
        res))))

(advice-add 'org-mw-link :around #'xwiki-link-syntax)

This is a custom command based on Aaron Bieber's org post, which adds a custom agenda view.

(setq org-agenda-custom-commands
      '(("d" "Daily agenda and all TODOs"
         ((tags "PRIORITY=\"A\""
                ((org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
                 (org-agenda-overriding-header "High-priority unfinished tasks:")))
          (alltodo ""
                   ((org-agenda-skip-function '(or (abrahms-org-skip-subtree-if-priority ?A)
                                                   (org-agenda-skip-entry-if 'todo '("WAITING"))
                                                   (org-agenda-skip-if nil '(scheduled deadline))))
                    (org-agenda-overriding-header "ALL normal priority tasks:")))
          (agenda "")
          (alltodo ""
                   ((org-agenda-skip-function '(org-agenda-skip-entry-if 'todo '("TODO")))
                    (org-agenda-overriding-header "Things I'm waiting on:"))))
         ((org-agenda-compact-blocks nil)))))

(defun abrahms-org-skip-subtree-if-priority (priority)
  "Skip an agenda subtree if it has a priority of PRIORITY.

PRIORITY may be one of the characters ?A, ?B, or ?C."
  (let ((subtree-end (save-excursion (org-end-of-subtree t)))
        (pri-value (* 1000 (- org-lowest-priority priority)))
        (pri-current (org-get-priority (thing-at-point 'line t))))
    (if (= pri-value pri-current)
        subtree-end
      nil)))

It's also interesting to play with org-super-agenda, which can generate filtered views into my org-file. Eventually, I hope to incorporate it with the custom agenda view above.

(use-package org-super-agenda
  :init (setq org-super-agenda-groups
              '(
                (:discard (:scheduled future))
                (:property ("PROJECT" "2022Goals") :name "2022 Goals" :order 12)
                (:property ("PROJECT" "2023Goals") :name "2023 Goals" :order 13)
                (:todo "NEXT")
                (:priority "A" :name "High priority")
                (:deadline t :name "Upcoming deadlines")
                (:scheduled today :name "today")
                (:scheduled past :name "past")
                (:property ("PROJECT" "bfcm") :name "Black Friday / Cyber Monday")
                (:property ("PROJECT" "dqa-fixes") :name "DQA Fixes")
                (:property ("PROJECT" "k8s") :name "k8s migration")
                (:property ("PROJECT" "open-feature") :name "Open Feature")
                (:property ("PROJECT" "testing-infra") :name "Testing Infrastructure")
                (:file-path "/notes.org" :order 11 :name "Home stuff")
                (:tag "writing" :order 10)
                (:discard (:scheduled future))
                (:auto-property "WAITING_ON" :log t :name "Waiting on people")
                (:auto-property "PROJECT" :log t)
                ;; (:auto-tags)
                (:auto-todo)))

  :config (org-super-agenda-mode 1)
  (variable-pitch-mode 1))

(use-package origami)

I need to fill out a weekly report of the sorts of stuff that I've been up to. This helps generate that using org-mode things.

(defun abrahms/weekly-report ()
  "What have I changed or added in the last 7 days?"
  (interactive)
  (let ((start (format-time-string "%Y-%m-%d" (time-add (current-time) (seconds-to-time (- (* 60 60 24 7))))))
        (end (format-time-string "%Y-%m-%d")))
    (org-ql-search
      (org-agenda-files)
      `(or
        (and (todo) (ts :from ,start :to ,end))
        (and (todo "DONE") (closed :from ,start :to ,end)))
      :sort '(todo reverse)
      )))

I've started to use org-datetree as my header entry. I don't want to log days, just weeks and their action items. This changes the datetree system to do that.

(eval-after-load "org-datetree"
  ;; This is a custom org-date tree which doesn't insert dates, just
  ;; week numbers. via https://emacs.stackexchange.com/a/60851/8777
  '(defun org-datetree-find-iso-week-create (d &optional keep-restriction)
     "Find or create an ISO week entry for date D.
Compared to `org-datetree-find-date-create' this function creates
entries ordered by week instead of months.
When it is nil, the buffer will be widened to make sure an existing date
tree can be found.  If it is the symbol `subtree-at-point', then the tree
will be built under the headline at point."
     (setq-local org-datetree-base-level 1)
     (save-restriction
       (if (eq keep-restriction 'subtree-at-point)
           (progn
             (unless (org-at-heading-p) (error "Not at heading"))
             (widen)
             (org-narrow-to-subtree)
             (setq-local org-datetree-base-level
                         (org-get-valid-level (org-current-level) 1)))
         (unless keep-restriction (widen))
         ;; Support the old way of tree placement, using a property
         (let ((prop (org-find-property "WEEK_TREE")))
           (when prop
             (goto-char prop)
             (setq-local org-datetree-base-level
                         (org-get-valid-level (org-current-level) 1))
             (org-narrow-to-subtree))))
       (goto-char (point-min))
       (require 'cal-iso)
       (let* ((year (calendar-extract-year d))
              (month (calendar-extract-month d))
              (day (calendar-extract-day d))
              (time (encode-time 0 0 0 day month year))
              (iso-date (calendar-iso-from-absolute
                         (calendar-absolute-from-gregorian d)))
              (weekyear (nth 2 iso-date))
              (week (nth 0 iso-date)))
         ;; ISO 8601 week format is %G-W%V(-%u)
         (org-datetree--find-create
          "^\\*+[ \t]+\\([12][0-9]\\{3\\}\\)\\(\\s-*?\
\\([ \t]:[[:alnum:]:_@#%%]+:\\)?\\s-*$\\)"
          weekyear nil nil
          (format-time-string "%G" time))
         (org-datetree--find-create
          "^\\*+[ \t]+%d-W\\([0-5][0-9]\\)$"
          weekyear week nil
          (format-time-string "%G-W%V" time))))))

I'd like it if graphviz dot files and others were auto-compile-able.

(org-babel-do-load-languages
 'org-babel-load-languages
 '((dot . t)
   (shell . t)
   (mscgen . t)
   (python . t)
   (plantuml . t)))

Support exporting to markdown.

(require 'ox-md)

Ditaa is a mechanism for turning ascii art into images. My emacsen doesn't ship with the jar it expects to have, so I install it with apt. This points it to the correct place.

(setq org-ditaa-jar-path "/usr/bin/ditaa")

Graphviz dotgraphs should use graphviz-dot-mode for syntax highlighting.

(use-package graphviz-dot-mode
  :after org
  :config
  (add-to-list 'org-src-lang-modes '("dot" . graphviz-dot))
  (unless (version<= emacs-version "26")
    (setq graphviz-dot-indent-width tab-width)))

Additionally, when doing org-babel, I want any images to show up inline. This is useful for rapidly building mscgen or graphviz graphs.

(add-hook 'org-babel-after-execute-hook 'org-display-inline-images)

I've begun experimenting with capturing notes from various buffers via capture mode. Setup where those notes go.

(setq org-default-notes-file (concat org-directory "/captured.org"))
(global-set-key (kbd "C-c c") 'org-capture)


(setq org-capture-templates
      ;; Explaination of values here:
      ;; https://orgmode.org/manual/Template-elements.html#Template-elements
      `(("t" "Todo" entry (file ,(concat org-directory "captured.org")) "**** TODO %?\n%a")
        ("m" "Meeting" entry (file ,(concat org-directory "captured.org")) "**** %?\n%t" )
        ("i" "Item" entry (file ,(concat org-directory "captured.org")) "**** %?\n%a" )
        ("a" "Action item" entry (file ,(concat org-directory "captured.org")) "* WAITING %?\n:PROPERTIES:\n:WAITING_ON: %^{Who owns this?}\n:END:\n %i %U %a")
        ("p" "Perf Note" entry (file ,(concat org-directory "captured.org")) "* %? :perf:\n\n %i %U" )
        ("c" "org-capture selected" entry (file ,(concat org-directory "captured.org"))
         "* [[%:link][%:description]]\n #+BEGIN_QUOTE\n%i\n#+END_QUOTE\n\n\n%?")
        ("C" "org-capture unselected" entry (file ,(concat org-directory "captured.org"))
         "* %? [[%:link][%:description]] \nCaptured On: %U")
        ))

I wrote my first emacs plugin to be listed on melpa (to my memory, at least) which hooks into org mode in order to output gemini sites.

(use-package ox-gemini)

Mermaid is a tool to generate nice sequence diagrams.

(use-package mermaid-mode)
(use-package ob-mermaid
  :after org
  :config (setq ob-mermaid-cli-path "/usr/local/bin/mmdc"))

When making TODO entries, give them a created time. I think this will be helpful to track down stale tasks & generally get metrics around throughput or similar when I bother to collect that data.

(use-package org-contrib)

(require 'org-expiry)

;; via https://stackoverflow.com/q/12262220/4972
(setq org-treat-insert-todo-heading-as-state-change t)
(add-hook 'org-after-todo-state-change-hook
          (lambda ()
            (when (string= org-state "TODO")
              (save-excursion
                (org-back-to-heading)
                (org-expiry-insert-created)))))

When refiling a headline, especially from my notes.. it's nice to leave a breadcrumb back to the place it was moved to so I can preserve the org-roam mappings. via

(defvar my/org-last-refile-marker nil)
(defvar my/org-last-refile-link nil)

(advice-add 'org-refile
            :before
            (lambda (&rest _)
              (save-excursion
                (org-back-to-heading)
                (setq my/org-last-refile-marker (point-marker))))
            '((name . "my/org-set-refile-marker")))

(defun my/org-set-last-refile-link ()
  (setq my/org-last-refile-link (org-store-link nil)))

(add-hook 'org-after-refile-insert-hook #'my/org-set-last-refile-link)

(advice-add 'org-refile
            :after
            (lambda (&rest _)
              (when (and my/org-last-refile-marker
                         my/org-last-refile-link)
                (let ((buf (marker-buffer my/org-last-refile-marker)))
                  (when (buffer-live-p buf)
                    (with-current-buffer buf
                      (save-excursion
                        (goto-char my/org-last-refile-marker)
                        (insert (concat my/org-last-refile-link "\n"))))))
                (setq my/org-last-refile-marker nil)
                (setq my/org-last-refile-link nil)))
            '((name . "my/org-insert-refile-marker")))

After Org

These things load after org is done.

(use-package org-superstar
  :after org
  :hook (org-mode . org-superstar-mode)
  :custom
  (org-superstar-remove-leading-stars t)
  (org-superstar-headline-bullets-list '("◉" "○" "●" "○" "●" "○" "●")))

(use-package org-appear
  :after org
  :hook (org-mode . org-appear-mode))

Setup the look and feel of org-mode such that we have a non-monospaced font in the text portions. Olivetti is a nice writing mode. Variable pitch is what does the work of making it mixed font.

(use-package olivetti
  :disabled
  :hook (org-mode . olivetti-mode))


(use-package org-variable-pitch
  :init (org-variable-pitch-setup)
  :hook (org-mode . org-variable-pitch-minor-mode))

org-modern, among other things, makes source blocks look really pretty in org-mode.

(use-package org-modern)

Graphs with gnuplot

(org-babel-do-load-languages
 'org-babel-load-languages
 '((gnuplot . t)))

I feel like markdown got it right and backticks are for surrounding code. To that end, this makes that work in emacs for both single backticks and triple ones.

;; via http://mbork.pl/2022-01-17_Making_code_snippets_in_Org-mode_easier_to_type
(defun org-insert-tilde ()
  "Insert a tilde using `org-self-insert-command'."
  (interactive)
  (if (string= (buffer-substring-no-properties (- (point) 3) (point))
               "\n~~")
      (progn (delete-char -2)
             (insert (format "#+begin_src %s\n#+end_src"
                             (read-string "Which language? " nil nil "text")))
             (forward-line -1)
             (org-edit-special))
    (setq last-command-event ?~)
    (call-interactively #'org-self-insert-command)))


(defun org-insert-backtick ()
  "Insert a backtick using `org-self-insert-command'."
  (interactive)
  (setq last-command-event ?`)
  (call-interactively #'org-self-insert-command))

(define-key org-mode-map (kbd "`") #'org-insert-tilde)
(define-key org-mode-map (kbd "~") #'org-insert-backtick)
  • Screenshots

    It's really helpful to be able to include screenshots in my org-mode setup. This seems to work for windows, though there are nice packages (org-screenshot, org-download) for the non-windows case.

    (require 'org-screenshot) ;; comes from org-contrib
    (customize-set-variable 'org-screenshot-image-directory (concat org-directory  "/images/"))
    
    (defun abrahms/joindirs (root &rest dirs)
      "Joins a series of directories together, like Python's os.path.join,
      (dotemacs-joindirs \"/tmp\" \"a\" \"b\" \"c\") => /tmp/a/b/c"
      ; via https://newbedev.com/what-is-the-correct-way-to-join-multiple-path-components-into-a-single-complete-path-in-emacs-lisp
    
      (if (not dirs)
          root
        (apply 'joindirs
               (expand-file-name (car dirs) root)
               (cdr dirs))))
    
    (defun ja/windows-screenshot ()
      "Take a screenshot into a time stamped unique-named file in a sub-directory of the org-buffer and insert a link to this file."
      ; via https://www.sastibe.de/2018/11/take-screenshots-straight-into-org-files-in-emacs-on-win10/
      (interactive)
      (setq filename
            (concat
             (make-temp-name
              (abrahms/joindirs (file-name-directory buffer-file-name)
                           "screenshots"
                           (concat (file-name-nondirectory buffer-file-name)
                                   "_"
                                   (format-time-string "%Y%m%d_%H%M%S_")) )) ".png"))
      ;; (message "filename: " filename)
      (let ((dirname (file-name-directory filename)))
        (message (concat "dirname: " dirname))
        (unless (file-exists-p dirname)
          (make-directory dirname)))
      (shell-command "snippingtool /clip")
      (shell-command (concat "powershell -command \"Add-Type -AssemblyName System.Windows.Forms;if ($([System.Windows.Forms.Clipboard]::ContainsImage())) {$image = [System.Windows.Forms.Clipboard]::GetImage();[System.Drawing.Bitmap]$image.Save('" filename "',[System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'clipboard content saved as file'} else {Write-Output 'clipboard does not contain image data'}\""))
      (insert (concat "[[file:" filename "]]"))
      (org-display-inline-images))
    

    org-screenshot has a requirement on scrot which doesn't exist as a binary for OSX. So install a simple wrapper around the screencapture tool it has.

    screencapture "$@"
    

    I like my screenshots to be named something slightly better than "screenshot-01.png", so this function will take the closest headline name and use that for the filename to write to.

    (defun alphanumericp (ch)
      (find ch "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"))
    
    (defun clean-filename (name)
      (remove-if-not (lambda (ch) (or (alphanumericp ch) (find ch "-_")))
                     (substitute ?- 32 name)))
    
    (defun abrahms/screenshot-filename (orig-fun &rest args)
      (let* ((doc-title (cadar (org-collect-keywords '("TITLE"))))
             (closest-headline (if (not (org-before-first-heading-p)) (nth 4 (org-heading-components))))
             (org-screenshot-file-name-format (format "%s-%%XXXX.png" (clean-filename (or doc-title closest-headline "screenshot")))))
        (apply orig-fun args)))
    
    (advice-add 'org-screenshot-take :around #'abrahms/screenshot-filename)
    
    
    

Org protocol

Org protocol is a mechanism where we can kick off emacs workflows (e.g. capturing todos) from external apps.

(require 'org-protocol)

We also need a desktop entry to tell Ubuntu how to load org-protocol links. When this is done, we must manually run update-desktop-database ~/.local/share/applications/

[Desktop Entry]
Name=org-protocol
Exec=/usr/bin/emacsclient -n %u
Type=Application
Terminal=false
Categories=System;
MimeType=x-scheme-handler/org-protocol;

You'll also need to change the preferences for the org-capture plugin to use the correct capture template. c is for selected, C is for unselected. If you want to prevent the "are you sure?" popups when you attempt to capture, turn security.external_protocol_requires_permission off.

Org roam

I've moved the bulk of my note taking to org-roam. In large part because building a personal wiki seems really valuable to me as it accumulates knowledge over the years.

(use-package org-roam
  :after org
  :bind (("C-c n l" . org-roam-buffer-toggle)
         ("C-c n f" . org-roam-node-find)
         ("C-c n g" . org-roam-graph)
         ("C-c n i" . org-roam-node-insert)
         ("C-c n c" . org-roam-capture)
         ;; Dailies
         ("C-c n j" . org-roam-dailies-capture-today))
  :init
  (setq org-roam-v2-ack t
        org-roam-directory (concat org-directory "roam/"))

  :config
  (org-roam-db-autosync-mode)

  (setq org-roam-capture-templates
        '(("c" "default" plain "%?"
         :if-new (file+head "${slug}.org" "#+title: ${title}\n#+date: %U\n")
         :unnarrowed t)))

  ;; link to my journal node on dailies insertion
  (setq org-roam-dailies-capture-templates
      '(("d" "Journal" entry "* %?"
         :if-new (file+head+olp "%<%Y-%m-%d>.org"
                                "#+title: %<%Y-%m-%d>\n[[id:81A8F7B2-5E27-4469-9473-97AAF9BFA104][journal]]\n"
                                ("Journal")))))

  ;; If using org-roam-protocol
  (require 'org-roam-protocol))

Deft is a tool which will help looking through a large number of files. I like it so far. My biggest complaint is that when you invoke it again after finding your item, it will still be populated with your last search phrase.

;; used to index notes. Can look at https://github.com/hasu/notdeft if that ever
;; gets slow.
(use-package deft
  :bind
  ("C-c n d" . deft)
  :config
  (setq
   deft-recursive t
   deft-use-filename-as-title t
   deft-use-filter-string-for-filename t
   deft-default-extension "org"
   deft-directory org-roam-directory))

;; capture screenshots
(use-package org-download
  :after org
  :custom
  ;;; pull screenshot from clipboard
  (org-download-screenshot-method "xclip -selection clipboard -t image/png -o > '%s'")
  :bind
  (:map org-mode-map
        (("s-Y" . org-download-screenshot)
         ("s-y" . org-download-yank))))

;; look into org-fc for spaced repitition


;; From org-roam-ui: https://github.com/org-roam/org-roam-ui#manually b/c they don't have a melpa release
(use-package websocket)
(use-package simple-httpd)

(use-package org-roam-ui
  :config
  (setq org-roam-ui-sync-theme t
        org-roam-ui-follow t
        org-roam-ui-update-on-save t
        org-roam-ui-open-on-start t))

(use-package org-roam-timestamps)

This vulpea stuff is an optimization that will allow limiting org-agenda to the roam files which have TODO entries. This will become more important over time as I accumulate a large number of entries.

;; vulpea stuff from https://d12frosted.io/posts/2021-01-16-task-management-with-roam-vol5.html
(use-package vulpea
  :after org-roam
  ;; hook into org-roam-db-autosync-mode you wish to enable
  ;; persistence of meta values (see respective section in README to
  ;; find out what meta means)
  :hook ((org-roam-db-autosync-mode . vulpea-db-autosync-enable)))

(defun vulpea-project-p ()
  "Return non-nil if current buffer has any todo entry.

TODO entries marked as done are ignored, meaning the this
function returns nil if current buffer contains only completed
tasks."
  (seq-find                             ; (3)
   (lambda (type)
     (eq type 'todo))
   (org-element-map                         ; (2)
       (org-element-parse-buffer 'headline) ; (1)
       'headline
     (lambda (h)
       (org-element-property :todo-type h)))))

(defun vulpea-project-update-tag ()
    "Update PROJECT tag in the current buffer."
    (when (and (not (active-minibuffer-window))
               (vulpea-buffer-p))
      (save-excursion
        (goto-char (point-min))
        (let* ((tags (vulpea-buffer-tags-get))
               (original-tags tags))
          (if (vulpea-project-p)
              (setq tags (cons "project" tags))
            (setq tags (remove "project" tags)))

          ;; cleanup duplicates
          (setq tags (seq-uniq tags))

          ;; update tags if changed
          (when (or (seq-difference tags original-tags)
                    (seq-difference original-tags tags))
            (apply #'vulpea-buffer-tags-set tags))))))

(defun vulpea-buffer-p ()
  "Return non-nil if the currently visited buffer is a note."
  (and buffer-file-name
       (string-prefix-p
        (expand-file-name (file-name-as-directory org-roam-directory))
        (file-name-directory buffer-file-name))))

(defun vulpea-project-files ()
    "Return a list of note files containing 'project' tag." ;
    (seq-uniq
     (seq-map
      #'car
      (org-roam-db-query
       [:select [nodes:file]
        :from tags
        :left-join nodes
        :on (= tags:node-id nodes:id)
        :where (like tag (quote "%\"project\"%"))]))))

(defun vulpea-agenda-files-update (&rest _)
  "Update the value of `org-agenda-files'."
  ;;; Not yet fully on org-roam, so also include things in the top level directory
  (setq org-agenda-files (append
                          (remove-if
                           (lambda (x)
                             (string-match (regexp-quote ".#") x))
                           (directory-files org-directory t "org$"))
                          (vulpea-project-files))))

(require 'vulpea)
(add-hook 'find-file-hook #'vulpea-project-update-tag)
(add-hook 'before-save-hook #'vulpea-project-update-tag)

(advice-add 'org-agenda :before #'vulpea-agenda-files-update)

When trying to find which of my projects were stuck, I look through all projects to determine if they have todo entries or not. I'd like to figure out a UI to display these, but not there quite yet.

(defun abrahms/get-project-from-org-file (f)
  (let ((go-back-to (current-buffer)))
    (find-file f)
    (let ((project (org-entry-get (point-min) "project" t)))
      (switch-to-buffer go-back-to)
      project)))

(defun abrahms/project-is-stuck (project-group)
  (let* ((project (nth 0 project-group))
        (files (seq-subseq project-group 1))
        (stuck-ones (seq-map 'abrahms/is-org-file-stuck files))
        (without-nulls (seq-remove 'null stuck-ones)))
    (if (< 0 (length without-nulls))
        (list project files))))

(defun abrahms/is-org-file-stuck (f)
  "given a filename, determine if it's stuck

Stuck means it has a 'project' property, but doesn't have TODOs"
  (let ((go-back-to (current-buffer)))
    (find-file f)
    (let* ((is-project (not (eq nil (org-entry-get (point-min) "project" t))))
           (is-deferred (org-entry-get (point-min) "deferred" t))
           (is-stuck (and is-project (not is-deferred) (not (vulpea-project-p)))))
      (switch-to-buffer go-back-to)
      is-stuck)))

(defun abrahms/stuck-projects ()
  "Gathers a list of org-files with a PROJECT property but without any associated TODOs"
  (interactive)
  (let* ((buffer-name "*stuck-projects*")
         (files-with-project
          (mapcar #'cadr (org-roam-db-query [:select * :from nodes :where (like properties (quote "%PROJECT%"))])))
         (grouped-by-project (seq-group-by 'abrahms/get-project-from-org-file files-with-project))
         (stuck-ones (seq-remove 'null  (seq-map 'abrahms/project-is-stuck grouped-by-project)))
         )

    (with-help-window buffer-name
      (princ "Stuck ones:\n")
      (seq-map (lambda (x)
                 (let ((project (nth 0 x))
                       (files (seq-subseq x 1)))
                   (princ (format "%s\n\t%s\n" project files))))
               stuck-ones))
    (switch-to-buffer-other-window buffer-name)))

There's not native support for refiling a subtree to a new node while going through the "name this appropriately" flow.

(defun abrahms/org-roam-refile-new-node ()
  ;; via https://org-roam.discourse.group/t/creating-an-org-roam-note-from-an-existing-headline/978
  "Create an Org-roam note from the current headline and jump to it.

Normally, insert the headline’s title using the ’#title:’ file-level property
and delete the Org-mode headline. However, if the current headline has a
Org-mode properties drawer already, keep the headline and don’t insert
‘#+title:'. Org-roam can extract the title from both kinds of notes, but using
‘#+title:’ is a bit cleaner for a short note, which Org-roam encourages."
  (interactive)
  (let ((title (nth 4 (org-heading-components)))
        (has-properties (org-get-property-block)))
    (org-cut-subtree)
    (org-roam-node-find 'other-window title nil)
    (org-paste-subtree)
    (unless has-properties
      (kill-line)
      (while (outline-next-heading)
        (org-promote)))
    (goto-char (point-min))
    (when has-properties
      (kill-line)
      (kill-line))))

When I review my notes, I'd like to choose one at random and just review & improve it. This is a method to function with that.

(defun abrahms/org-roam-random ()
  "Finds random org roam file"
  (interactive)
  (find-file (seq-random-elt (org-roam-list-files))))

Org & PDFs

In order to take org-roam notes on PDFs, I need the tools for pdf viewing (pdf-tools), mechanisms of linking it with org files (org-pdftools).

;; (use-package pdf-tools
;;   :ensure-system-package (poppler automake)
;;   :config (pdf-loader-install))

;; (use-package org-pdftools
;;   :after pdf-tools
;;   :hook (org-mode . org-pdftools-setup-link))

I've also included citar for citing things. I don't know if that will be useful, but it seems like I might want that info later, so I'll start collecting it. It operates on biblatex files.

(use-package citar
  :no-require
  :custom
  (org-cite-global-bibliography (list (concat org-directory "/references.bib")))
  (org-cite-insert-processor 'citar)
  (org-cite-follow-processor 'citar)
  (org-cite-activate-processor 'citar)
  (citar-bibliography org-cite-global-bibliography)
  ;; optional: org-cite-insert is also bound to C-c C-x C-@
  :bind
  (:map org-mode-map :package org ("C-c b" . #'org-cite-insert)))


(use-package org-ref
  :config
  (setq
   org-ref-completion-library 'org-ref-ivy-cite
   ;; org-ref-get-pdf-filename-function 'org-ref-get-pdf-filename-helm-bibtex
   org-ref-default-bibliography (list (concat org-directory "/references.bib"))
   org-ref-note-title-format "* TODO %y - %t\n :PROPERTIES:\n  :Custom_ID: %k\n  :NOTER_DOCUMENT: %F\n :ROAM_KEY: cite:%k\n  :AUTHOR: %9a\n  :JOURNAL: %j\n  :YEAR: %y\n  :VOLUME: %v\n  :PAGES: %p\n  :DOI: %D\n  :URL: %U\n :END:\n\n"
   org-ref-notes-directory (concat org-directory "/roam/")
   org-ref-notes-function 'orb-edit-notes


   bibtex-align-at-equal-sign t ; prettier formatting
   bibtex-dialect 'biblatex     ; bibtex is apparently very old (1988), so use the new variant

   ;; tell org to output using bibtex
   org-latex-pdf-process (list "latexmk -shell-escape -bibtex -f -pdf %f")

   bibtex-user-optional-fields '(("keywords" "Keywords to describe the entry" "")
                                 ("file" "link to the document file" ":")
                                 ("url" "url of the thing" ""))
   bibtex-completion-bibliography (list (concat org-directory "/references.bib"))
   bibtex-completion-library-path (list (concat org-directory "/pdfs_for_notes/"))
   bibtex-completion-pdf-field "file"

   ;; Not sure about this yet..
   ;; bibtex-completion-notes-template-multiple-files
   ;; (concat
   ;;  "#+TITLE: ${title}\n"
   ;;  "#+ROAM_KEY: cite:${=key=}\n"
   ;;  "* TODO Notes\n"
   ;;  ":PROPERTIES:\n"
   ;;  ":Custom_ID: ${=key=}\n"
   ;;  ":NOTER_DOCUMENT: %(orb-process-file-field \"${=key=}\")\n"
   ;;  ":AUTHOR: ${author-abbrev}\n"
   ;;  ":JOURNAL: ${journaltitle}\n"
   ;;  ":DATE: ${date}\n"
   ;;  ":YEAR: ${year}\n"
   ;;  ":DOI: ${doi}\n"
   ;;  ":URL: ${url}\n"
   ;;  ":END:\n\n"
   ;;  )
   )
  )

(use-package citeproc)

(use-package bibtex
  :config
  (setq bibtex-autokey-year-length 4
        bibtex-autokey-name-year-separator "-"
        bibtex-autokey-year-title-separator "-"
        bibtex-autokey-titleword-separator "-"
        bibtex-autokey-titlewords 2
        bibtex-autokey-titlewords-stretch 1
        bibtex-autokey-titleword-length 5))

(use-package ivy-bibtex
  :config
  (autoload 'ivy-bibtex "ivy-bibtex" "" t)
  ;; ivy-bibtex requires ivy's `ivy--regex-ignore-order` regex builder, which
  ;; ignores the order of regexp tokens when searching for matching candidates.
  ;; Add something like this to your init file:
  (setq ivy-re-builders-alist
        '((ivy-bibtex . ivy--regex-ignore-order)
          (t . ivy--regex-plus))))

;; most of this config taken from http://www.wouterspekkink.org/academia/writing/tool/doom-emacs/2021/02/27/writing-academic-papers-with-org-mode.html
;; (use-package org-roam-bibtex
;;   :commands org-roam-bibtex-mode
;;   :after (org-roam org-ref)
;;   :hook org-roam-mode
;;   :bind (:map org-mode-map ("C-c n b" . orb-note-actions)) ;; yes, really `orb`.
;;   :config
;;   (require 'org-ref)
;;   (org-roam-bibtex-mode)
;;   (setq orb-preformat-keywords '("citekey" "title" "url" "author-or-editor" "keywords" "file")
;;         orb-process-file-keyword t
;;         orb-file-field-extensions '(pdf))

;;   (setq orb-templates
;;         '(("r" "ref" 'org-roam-capture--get-point ""
;;            :file-name "${citekey}"
;;            :head "#+TITLE: ${citekey}: ${title}\n#+ROAM_KEY: ${ref}
;; - tags ::
;; - keywords :: ${keywords}

;; * Notes
;; :PROPERTIES:
;; :Custom_ID: ${citekey}
;; :URL: ${url}
;; :AUTHOR: ${author-or-editor}
;; :NOTER_DOCUMENT: ${file}
;; :NOTER_PAGE:
;; :END:")))
;;   )

Org-noter lets you take notes on pdfs. I'd like for it to make org-roam documents, but it doesn't seem to support that by default.

(defun abrahms/get-notes-for-source-file ()
  "org-noter doesn't play nicely with org-roam for creating a roam node when we want to go from PDF -> notes. This provides that facility."
  ;; TODO: Figure out how to guard this from being called in places it shouldn't be.
  (interactive)
  ;; get filename for doc which references this file
  (let* ((source-file (buffer-file-name))
         (notes-file-for-library-file (first (org-ql-query
                                               :select (lambda () (org-entry-get nil "FILE"))
                                               :from org-agenda-files
                                               :where `(property "NOTER_DOCUMENT" ,source-file)))))
    (if (not notes-file-for-library-file)
        ;; (let ((prop-hook-once (lambda ()
        ;;                         (org-roam-add-property source-file "NOTER_DOCUMENT")
        ;;                         (remove-hook 'org-roam-capture-new-node-hook (first org-roam-capture-new-node-hook)))))
        ;;   ;; TODO: I suspect this hook will persist if I cancel the capture.
        ;;   (add-hook 'org-roam-capture-new-node-hook prop-hook-once)
          (org-roam-capture-
           :node (org-roam-node-read)
           :templates '(("d" "default" plain "%?"
                         :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
                                            (concat "#+title: ${title}\n"
                                                    "* ${title}\n"
                                                    ":PROPERTIES:\n"
                                                    ":NOTER_DOCUMENT: " source-file
                                                    ":END:"))
                         :unnarrowed t
                         )))
                                        ;)
      (find-file notes-file-for-library-file))))



(use-package org-noter
  :after (org pdf-tools)
  :config (setq
           org-noter-notes-search-path (list (concat org-directory "/roam/"))
           org-noter-always-create-frame nil
           org-noter-separate-notes-from-heading t
           org-noter-hide-other nil)
  :init
  ;; When pdfs are viewed, add a shortcut to get notes.
  (define-key pdf-view-mode-map (kbd "i") 'abrahms/get-notes-for-source-file))


(use-package org-ql)

;; WIP: I think to build the org-roam setup, I'd want to query all org-roam
;; notes for anything which references NOTER_DOCUMENT property. If nothing
;; exists, capture a new org-roam note, then add in the necessary properties
;; into the resulting document.

github issues as TODOs

(use-package org-sync
    :straight (org-sync
             :type git
             :host github
             :repo "arbox/org-sync")
    :init (load "org-sync-github"))

synchronization

I primarily sync my org files via Dropbox. Sometimes, this results in conflicted copies of files. This script will detect any differences between them so I can ensure I don't lose data.

# Sometimes dropbox gets grumpy w/ mutli edits. This allows me to diff any
# conflicted files.

IFS=$'\n'
CONFLICTED=$(find ~docs/roam/ -name "*conflicted*.org")
for i in $CONFLICTED; do
    echo "== $i =="
    diff "$i" "${i// (*)/}"
    echo
    echo
done

subconscious

A few random tools that help syncing org-roam output w/ subconscious.network

(defun abrahms/roam-add-share-tag () (interactive) (org-roam-tag-add '("share")))
(defun abrahms/roam-add-ignore-tag () (interactive) (org-roam-tag-add '("ignore")))
(defun orb-status ()
  (interactive)
  (shell-command "cd ~/noosphere/sphere/.utils; nix run '.#devshell' -- render; cd ..; orb sphere status"))

(defun orb-save-and-sync ()
  (interactive)
  (shell-command "cd ~/noosphere/sphere/; orb sphere save; orb sphere sync"))

Publishing

Writing out org files into a local markdown setup is useful. Some for my notes, but also for powering local LLMs who only understand markdown.

(use-package ox-hugo
  :ensure t
  :pin melpa
  :config (setq org-hugo-front-matter-format "yaml")
  :after ox)


(defun abrahms/org-get-filetags (e)
  ; per https://emacs.stackexchange.com/questions/75952/how-can-i-retrieve-org-file-meta-data-filetags-from-an-org-element-parse-buff
  (let ((type (org-element-type e))
        (key (org-element-property :key e))
        (value (org-element-property :value e)))
    (if (and (eq type 'keyword) (equal key "FILETAGS"))
        (split-string value ":"))))

(defun flatten-list (list-of-lists)
  "Flatten a LIST-OF-LISTS into a single list."
  (apply 'append list-of-lists))

(require 'cl-lib)
(defun string-lists-intersect-p (list1 list2)
  "Return t if LIST1 and LIST2 have any elements in common, nil otherwise."
  (not (null (cl-intersection list1 list2 :test 'string=))))

;; TODO: There's an odd interaction w/ file tags & headlines. Removing file tags works, but that's not quite what I'd prefer to happen.
(defun abrahms/ox-hugo-org-publish (plist filename pub-dir)
  (message "Was asked to export %s" filename)
  ;;; skip dailies
  (if (not (string-match-p (regexp-quote "/daily/") filename))
      (with-current-buffer (find-file-noselect filename)

        (let ((tags (flatten-list (org-element-map (org-element-parse-buffer) '(keyword) #'abrahms/org-get-filetags))))
          (if (not (string-lists-intersect-p tags org-export-exclude-tags))
              (org-hugo--export-file-to-md filename)
            (message "Didn't export. Found one of the exclude tags (%s) in the file's tags (%s)" org-export-exclude-tags tags)
            )))))

(setq
 ob-mermaid-cli-path "/opt/homebrew/bin/mmdc"
 org-hugo-base-dir "~/notes-stale/"
 org-publish-project-alist
      `(("markdown"
         :base-directory ,org-roam-directory
         :publishing-directory ,hugo-base-dir
         :select-tags '()
         :recursive t
         :publishing-function abrahms/ox-hugo-org-publish
         :author "Justin Abrahms"
         :email "justin@abrah.ms"
         ))
      org-export-with-broken-links t
      org-export-with-tasks nil
      org-export-with-tags nil
      org-hugo-auto-set-lastmod t
      )

Markdown

Live-markdown previews are quite useful when editing large documents. flymd offers live preview, but doesn't work for org-mode source regions natively. Because of this, we need to change their "is this a markdown file?" regex slightly to include the string "markdown".

(use-package markdown-mode
  :commands markdown-mode
  :init
  (add-hook 'markdown-mode-hook #'visual-line-mode)
  (add-hook 'markdown-mode-hook #'variable-pitch-mode)
  (add-hook 'markdown-mode-hook #'flyspell-mode)
  :config
  (setq flymd-markdown-regex (mapconcat 'identity '("\\.md\\'" "\\.markdown\\'" "markdown") "\\|"))

  ;; The default command for markdown (~markdown~), doesn't support tables
  ;; (e.g. GitHub flavored markdown). Pandoc does, so let's use that.
  (setq markdown-command "pandoc --from markdown --to html")
  (setq markdown-command-needs-filename t))

(use-package flymd
  :hook markdown-mode
  :commands flymd-flyit
  :requires markdown-mode)

Text

There are common technical words that aren't a part of the dictionary. Provide a handy shortcut for adding them.

   (require 'flyspell)
   (defun abrahms/flyspell-save-word ()
     (interactive)
     (let ((current-location (point))
            (word (flyspell-get-word)))
       (when (consp word)
         (flyspell-do-correct 'save nil (car word) current-location (cadr word) (caddr word) current-location))))

(use-package jinx
  :hook (emacs-startup . global-jinx-mode)
  :bind ([remap ispell-word] . jinx-correct))

When editing raw text, it's nice to have a simple spellcheck. For this, we use flyspell.

(define-key flyspell-mode-map (kbd"\C-c$") 'abrahms/flyspell-save-word)

LaTeX & graphing

cdlatex does a great job of making it really easy to inline latex such as for math taking notes.

;; Make latex previews large & legible
(setq org-format-latex-options (plist-put org-format-latex-options :scale 2.0))
(use-package cdlatex
  :after org
  :hook (org-mode . turn-on-org-cdlatex)
  :config
  (defun cdlatex-insert-newline ()
    "In the context of a latex fragment, insert a line continuation and go to the next line"
    (interactive)
    (if (texmathp)
        (progn
          (end-of-line)
          (insert "\\\\")
          (newline-and-indent))
      (org-meta-return)))

  (define-key org-cdlatex-mode-map (kbd "M-RET") 'cdlatex-insert-newline)
  ;;; Can't have it deleting my ` keybinding, but math-symbol is useful so remap it.
  (define-key cdlatex-mode-map (kbd "`") nil)
  (define-key org-cdlatex-mode-map (kbd "`") nil)
  (define-key org-cdlatex-mode-map (kbd "M-`") 'cdlatex-math-symbol))

I also find the need to replicate graphs. gnuplot is kinda terrible at math notation, so I'm hoping asymptote (a thing related to latex) works for it.

(use-package asy-mode
    :straight (asy-mode
             :type git
             :host github
             :files ("base/*.el")
             :repo "vectorgraphics/asymptote")
  :after org
  :config
  (org-babel-do-load-languages
   'org-babel-load-languages
   '((asymptote . t)))
  )

;; https://github.com/raxod502/straight.el/issues/240#issuecomment-367468389
(use-package tex
  :straight auctex)

;; Fragtog makes it so when you enter a latex fragment, it is editable text. When you leave, it's rendered into a latex preview.
(use-package org-fragtog
  :hook (org-mode . org-fragtog-mode))

Taking notes in my linear algebra class is a pain b/c writing latex matrices is.. hard. This is a function which helps with that.

(defun latex-matrixify ()
  "turn a shorthand matrix of [2,3,4|2\\9,2,1|9] into the proper latex form"
  (interactive)
  (if (not (use-region-p))
      (message "You have to select a region to operate on.")
    (let ((pairs (list
                  (cons "]" "\\end{bmatrix}")
                  (cons "[" "\\begin{bmatrix}")
                  (cons "," " & ")
                  (cons "\\" "\\
")
                  (cons "|" " & | &")
                        )))
      (dolist (p pairs)
        (message "beg: %s" (region-beginning))
        (message "end: %s" (region-end))
        (let ((beg   (save-excursion (goto-char (region-beginning))
                                     (line-beginning-position)))
              (end   (save-excursion (goto-char (region-end)) (line-end-position))))
          (message "beg2: %s" beg)
          (message "end2: %s" end)

          (replace-string-in-region (car p) (cdr p) beg end))))))

Scratchpad

Sometimes it's nice to have a scratch buffer. This lets you do that, but also have the relevant mode turned on.

(use-package scratch)

Grammar checking

Language Tool is an open source alternative to grammarly. I'm hoping it will improve my writing.

(use-package langtool
  :straight (langtool
             :type git
             :host github
             :repo "mhayashi1120/Emacs-langtool")
  :init
  (setq langtool-http-server-host "localhost"
        langtool-http-server-port 8081))

Spaced Repetition

When taking notes with org, it's sometimes useful (especially in school contexts) to do spaced repetition of certain topics. org-drill helps with that.

(use-package org-drill
  :config
  (setq org-drill-hide-item-headings-p t))

Diagramming

PlantUML is also a good diagramming tool. Add support for that.

(use-package plantuml-mode
  :config (setq plantuml-default-exec-mode "jar"
                plantuml-jar-path (expand-file-name "~/plantuml-1.2021.15.jar")
                org-plantuml-jar-path plantuml-jar-path)
  (add-to-list 'org-src-lang-modes '("plantuml" . plantuml))
  (org-babel-do-load-languages 'org-babel-load-languages '((plantuml . t))))

Blogging

I'm slowly porting over to using org-static-blog as it has some blog-centric output which is quite helpful.

(use-package org-static-blog
  :after org
  :config (load "~/.config/emacs/static-blog.el"))

I keep the static blog separate so it's easier to load just this config for publishing.

(package-initialize)
(require 'org)
(require 'org-static-blog)
(use-package gnuplot)

(org-babel-do-load-languages
 'org-babel-load-languages
 '((gnuplot . t)))

;; In the Makefile, this is set dynamically.
(if (not (boundp 'blog-root))
    (setq blog-root "~/src/justin.abrah.ms/blog"))

(setq org-static-blog-publish-title "Justin Abrahms"
      org-static-blog-publish-url "https://justin.abrah.ms/"
      org-static-blog-publish-directory "/srv/justin.abrah.ms"
      org-static-blog-rss-file "/rss.xml"
      org-static-blog-posts-directory  (concat blog-root "/_posts")
      org-static-blog-drafts-directory (concat blog-root "/_drafts")
      org-static-blog-enable-tags t
      org-export-with-toc nil
      org-export-with-section-numbers nil
      org-static-blog-page-preamble (concat "<nav><a href=\"" org-static-blog-publish-url "\">Home</a></nav>
<nav class=\"fontawesome\">
    <a rel=\"me\" href=\"https://hachyderm.io/@abrahms\">
        <i title=\"Mastodon\" class=\"fab fa-mastodon\"></i>
    </a>
    <a href=\"mailto:justin@abrah.ms\" target=\"_blank\">
        <i title=\"Email\" class=\"fas fa-at\"></i>
    </a>
    <a href=\"" org-static-blog-rss-file "\" target=\"_blank\">
        <i title=\"RSS\" class=\"fas fa-rss\"></i>
    </a>
  </nav>
")
      org-static-blog-page-header (concat "
<link rel='stylesheet' href='/static/css/blog.css'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta charset='utf-8'>
<link rel='apple-touch-icon' sizes='180x180' href='/static/apple-touch-icon.png'>
<link rel='icon' type='image/png' sizes='32x32' href='/static/favicon-32x32.png'>
<link rel='icon' type='image/png' sizes='16x16' href='/static/favicon-16x16.png'>
<link rel='manifest' href='/site.webmanifest'>
<link rel='alternate' type='application/rss+xml' href='/rss.xml' title='Justin Abrahms'>
<link href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css' rel='stylesheet'>
")
      org-static-blog-index-file "no-one-cares"
      org-static-blog-archive-file "index.html"
      org-static-blog-page-postamble (concat
                                      " <div class='foot'>
    &copy; 2012 - 2023 &#183;
    <a href=\"" org-static-blog-publish-url "\" >Home</a> &mdash; Theme <a href='https://github.com/RainerChiang/simpleness'>Simpleness</a>
    <a href='#'><i class='fas fa-chevron-up'></i></a>
</div>"))

(defun org-static-blog-assemble-archive ()
  "Re-render the blog archive page.
The archive page contains single-line links and dates for every
blog post, but no post body."
  (let ((archive-filename (concat-to-dir org-static-blog-publish-directory org-static-blog-archive-file))
        (archive-entries nil)
        (post-filenames (org-static-blog-get-post-filenames)))
    (setq post-filenames (sort post-filenames (lambda (x y) (time-less-p
                                                             (org-static-blog-get-date y)
                                                             (org-static-blog-get-date x)))))
    (org-static-blog-with-find-file
     archive-filename
     (org-static-blog-template
      org-static-blog-publish-title
      (concat
       "<ul class=\"archive\">"
       (apply 'concat (mapcar (lambda (post-filename)
                                (concat
                                 "<li>"
                                 "<time>" (format-time-string (org-static-blog-gettext 'date-format) (org-static-blog-get-date post-filename))"</time>"
                                 "<a href=\"" (org-static-blog-get-post-url post-filename)  "\">"
                                 (org-static-blog-get-title post-filename)
                                 "</a>"
                                 "</li>")
                                ) post-filenames))
       "</div>")))))

(defun org-static-blog-post-preamble (post-filename)
  "Returns the formatted date and headline of the post.
This function is called for every post and prepended to the post body.
Modify this function if you want to change a posts headline."
  (concat
   "<h1 class=\"post-title\">"
   (org-static-blog-get-title post-filename)
   "</h1>\n"
   "<div class=\"post-meta\"><time>"
   (format-time-string (org-static-blog-gettext 'date-format)
                       (org-static-blog-get-date post-filename))
   "</time>"
   "<div class=\"taglist\">"
   "<i class=\"fas fa-folder\"></i>"
   (org-static-blog-post-taglist post-filename)
   "</div>"
   "</div>"))

(defun org-static-blog-post-postamble (post-filename)
  "Returns the tag list and comment box at the end of a post.
This function is called for every post and the returned string is
appended to the post body, and includes the tag list generated by
followed by the HTML code for comments."
  (concat
   (if (string= org-static-blog-post-comments "")
       ""
     (concat "\n<div id=\"comments\">"
             org-static-blog-post-comments
             "</div>"))))

This is the CSS I use for the website.

@media (min-width: 50em) {
  @font-face {
    font-family: 'PT Sans';
    src: local("PT Sans"), local("PTSans-Regular"), url("/static/fonts/PTSans-Regular.ttf");
    font-weight: normal;
    font-style: normal;
  }
  @font-face {
    font-family: 'PT Sans';
    src: local("PT Sans Bold"), local("PTSans-Bold"), url("/static/fonts/PTSans-Bold.ttf");
    font-weight: bold;
    font-style: normal;
  }
}

@media (prefers-color-scheme: light) {
  html {
    background: #ffffff;
    color: #1c1d22;
  }

  #content a {
    color: #0000EE;
  }

  .foot a:hover {
    text-decoration: underline;
    color: #0000EE;
  }
}

@media (prefers-color-scheme: dark) {
  html {
    background: #1c1d22;
    color: #ffffff;
  }

  article a, #content a {
    color: #58A6FF;
  }

  .foot a:hover {
    text-decoration: underline;
    color: #58A6FF;
  }
}

html {
  font-size: 16px;
  font-size: calc(0.8rem + 0.3vw);
}

body {
  font-family: "PT Sans", -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Segoe UI", "Roboto", sans-serif;
  font-weight: light;
  line-height: 1.5;
  margin: 0;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-size: 14px;
  counter-reset: section;
}

h1, h2, h3, h4, h5, h6 {
  font-weight: 600;
}

b, strong, th {
  font-weight: 600;
}

a {
  color: inherit;
  text-decoration: inherit;
}

a:hover {
  text-decoration: underline;
}

header time, .post-date, .taglist {
  color: #8e8e91;
}

.taglist {
  display: inline;
}

.taglist > .fa-folder {
  padding: 0 10px;
}

hr {
  border: 1px solid rgba(142, 142, 145, 0.3);
  margin: 2em 0;
}

article:not(:last-child) {
  border-bottom: 1px solid rgba(142, 142, 145, 0.12);
}

blockquote {
  background: rgba(142, 142, 145, 0.06);
  border-left: 3px solid rgba(142, 142, 145, 0.9);
  padding: 1px 1.5em;
  /* opacity: .75; */
}

blockquote, figure {
  margin: 1em 0;
}

img, li {
  margin: .5em 0;
}

img {
  border-radius: 5px;
  max-width: 100%;
  height: auto;
  /* center horizontally */
  margin-left: auto;
  margin-right: auto;
  display: block;
}

table {
  width: 100%;
  border-spacing: 1px;
  box-shadow: 0 0 0 1px rgba(142, 142, 145, 0.12) inset;
}

th, td {
  padding: .5em 1em;
  box-shadow: 0 0 0 1px rgba(142, 142, 145, 0.12);
}

tr:hover, tr:nth-child(odd) td {
  background: rgba(142, 142, 145, 0.04);
}

pre {
  background: rgba(142, 142, 145, 0.12);
  border-radius: 2px;
  font-size: .8em;
  /* font-family: fira mono,cousine,Monaco,Menlo,source code pro,monospace; */
  font-family: 'Courier New', Courier, monospace;
  margin: 1.5em 0;
  padding: .8em 1.2em;
  overflow-x: auto;
}

:not(pre) > code {
  font-size: .9em;
  background: rgba(142, 142, 145, 0.15);
  /* opacity: .7; */
  border-radius: 2px;
  margin: 0 .1em;
  padding: .2em .4em;
}

body > header, body > #preamble {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  background: rgba(255, 255, 255, 0.02);
  box-shadow: 0 0 .6em rgba(28, 29, 34, 0.05);
  border-bottom: 1px solid rgba(142, 142, 145, 0.16);
}

body > header > a, body > #preamble > a {
  font-size: 1.3em;
}

article header, #content #preamble {
  margin-bottom: 1.5em;
}

article header h1, #content #preamble h1 {
  font-size: 1.8em;
  margin: .5em .5em;
}

article h2, #content h2 {
  padding-bottom: 0.3em;
  font-size: 1.5em;
  border-bottom: 1px solid #eaecef;
}

nav {
  margin: .5em -.8em;
}

nav a {
  margin: .5em .8em;
}

footer hr { /* custom */
  width: 45%;
  border-style: dashed;
  margin-bottom: 1em;
}

body > header, body > #preamble, body > article, body>#content, body > footer {
  padding: 1.5em;
}

@media (min-width: 32em) {
  body > header, body > #preamble, body > article, body > #content, body > footer {
    padding: 1.75em calc(38% - 12em);
  }
}

.archive li {
  font-size: 1.1em;
  list-style: none;
}

.archive time, .archive post-date {
  display: inline-block;
  min-width: 10ch;
  margin: 0 .2em;
  font-family: monospace;
  padding-right: 1em;
}

.hidden {
  display: none;
}

.more {
  margin: 2em 0 1em;
}

.more a {
  border-radius: 2px;
  border: 1.5px solid #68f;
  padding: .4em .8em;
  transition: .2s;
}

.more a:hover {
  color: #fff;
  background: #68f;
  text-decoration: inherit;
}

/***** custom *****/
.fontawesome a {
  text-decoration: none;
}

.post-meta {
  color: #8e8e91;
}

.comments-item {
  margin-top: 10%;
}

.foot {
  font-family: "PT Sans", -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Segoe UI", "Roboto", sans-serif;
  margin: 1%;
  color: #8e8e91;
  text-align: center;
}

body {
}

/* --- Table of contents --- */
#table-of-contents {
    padding-top: 0em;
    margin-top: 0em;
    text-transform: uppercase;
}
#table-of-contents ul {
    padding: 0;
    font-weight: 400;
    list-style: none;
    counter-reset: list 0;
}
#table-of-contents ul ul {
    padding-left:0em;
    font-weight: 300;
    font-size: 90%;
    line-height: 1.5em;
    margin-top: 0em;
    margin-bottom: 1em;
    padding-left: 2em;
}
#table-of-contents h2:before {
    content: "";
    counter-reset: section;
}
#table-of-contents ul li {
    vertical-align: top;
    display: inline-block;
    width: 32%;
}
#table-of-contents ul li:before {
    display: inline-block;
    counter-increment: list;
    content: counters(list, ".") ".";
    width: 2.0em;
    margin-left: -2.0em;
    text-align: right;
    text-transform: uppercase;
    color:#2255bb;
}
#table-of-contents ul li ul,
#table-of-contents ul li ul li {
    display: static;
    width: 100%;
    padding-left: 0;
    line-height: 1.35em;
}
#table-of-contents h2 {
    font-size: 1em;
    font-weight: 400;
}
#table-of-contents ul li ul li::before {
    content: "";
}

/* Section numbering */
body {
  counter-reset: section;
}
h2 {
  counter-reset: subsection;
}
h2::before {
    color: #cccccc;
    float: left;
    text-align: right;
    font-weight: 300;
    width: 7.5em;
    margin-left: -8.0em;
    counter-increment: section;
    content: "Chapter " counter(section) " ";
}

h3::before {
    color: #cccccc;
    float: left;
    text-align: right;
    font-weight: 300;
    width: 2.5em;
    margin-left: -3.0em;
    counter-increment: subsection;
    content: counter(section) "." counter(subsection) " ";
}
/** end section numbering **/

code {
    background-color: #f9f9f9;
    font-family: 'Roboto Mono', monospace;
    font-weight: 400;
}

pre::before {
    color: #cccccc;
    float: left;
    text-align: right;
    font-weight: 300;
    width: 3.0em;
    margin-left: -4.25em;
    font-variant: small-caps;
    content: '';
}
@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:300,400,500');

pre.src-emacs-lisp::before { content: 'elisp'; }
pre.src-org::before { content: 'org'; }
pre.src-java::before { content: 'java'; }
pre.src-bash::before, pre.src-sh::before { content: 'shell'; }

pre {
    overflow: auto;
    margin: 0em;
    padding: 0.25em;
    padding-left: 0.5em;
    line-height: 1.35em;
    font-family: 'Roboto Mono', monospace;
    font-weight: 300;
    border-left: 2px dotted #58a6ff;
}
pre.src {
    position: relative;
    overflow: visible;
}

Programming Languages

General

Ensure our files don't have weird trailing whitespace.

(use-package ws-butler
  :hook ((text-mode . ws-butler-mode)
         (prog-mode . ws-butler-mode)))

Setup editorconfig for projects that use that.

(use-package editorconfig
  :config
  (editorconfig-mode 1)
  (setq editorconfig-trim-whitespaces-mode
        'ws-butler-mode))

Highlight things which are under the point.

(use-package idle-highlight-mode
  :hook prog-mode)

Makefiles must use tabs or they get cranky.

(add-hook 'makefile-mode-hook
  '(lambda()
     (setq indent-tabs-mode t)
   )
)

Compilation modes

I use compilation-mode often when running quick tests for a module or similar. Set it up so it's nicer to work with.

(use-package compile
  :config
  (setq compilation-scroll-output 'first-error))

(defun auto-recompile-buffer ()
  (interactive)
  (if (member #'recompile after-save-hook)
      (remove-hook 'after-save-hook #'recompile t)
    (add-hook 'after-save-hook #'recompile nil t)))

;; colorize the output of the compilation mode.
(use-package ansi-color)
(defun colorize-compilation-buffer ()
  (toggle-read-only)
  (ansi-color-apply-on-region (point-min) (point-max))

  ;; mocha seems to output some non-standard control characters that
  ;; aren't recognized by ansi-color-apply-on-region, so we'll
  ;; manually convert these into the newlines they should be.
  (goto-char (point-min))
  (while (re-search-forward "\\[2K\\[0G" nil t)
    (progn
      (replace-match "
  ")))
  (toggle-read-only))

(add-hook 'compilation-filter-hook 'colorize-compilation-buffer)

Language servers

I'm going to experiment with language servers, using lsp-mode specifically. This is a pretty nice recent trend in languages, so you can externalize a lot of the particularities of programming languages.

   (use-package lsp-mode
     :init (setq lsp-keymap-prefix "C-;")
     :hook (((js2-mode yaml-mode go-mode typescript-mode) . lsp)
            (lsp-mode . lsp-enable-which-key-integration))
     :config
     (setq lsp-completion-enable t)
     (progn
       (define-key lsp-mode-map (kbd "M-.") 'lsp-goto-implementation)
       (let ((eslint-language-server-file "/home/abrahms/src/github.com/microsoft/vscode-eslint/server/out/eslintServer.js"))
         (if (file-exists-p eslint-language-server-file)
             (setq lsp-eslint-server-command '("node" eslint-language-server-file "--stdio"))
           (message "You're missing the vscode eslint plugin. Currently, you have to manually install it."))))
     :commands lsp lsp-deferred)

(use-package company) ; used for lsp autocompletion

   (use-package lsp-python-ms
     :init (setq lsp-python-ms-auto-install-server t)
     :hook (python-mode . (lambda ()
                            (require 'lsp-python-ms)
                            (lsp))))

   (use-package lsp-ui
     :after lsp
     :hook lsp-mode
     ;; flycheck integration & higher level UI modules
     :commands lsp-ui-mode
     :config
     (setq lsp-ui-sideline-enable t)
     (setq lsp-ui-sideline-show-hover nil)
     (setq lsp-ui-doc-position 'bottom)
     (lsp-ui-doc-show))

   (use-package lsp-treemacs
     ;; project wide overview
     :commands lsp-treemacs-errors-list)

   (use-package dap-mode
     :commands (dap-debug dap-debug-edit-template))

   (use-package which-key
     :config (which-key-mode))

Lisp

Paredit is a really neat lisp editing mode. One big thing it does for you is keep your parens balanced at all times, which turns out to be pretty important. It has a lot of mini-refactoring commands built-in (pull up, add parameter, etc) but I always forget them because I don't write lisp enough to commit it to memory.

eldoc mode is will update your minibuffer to show the parameters the function under your cursor takes, which can be a helpful for jogging your memory.

(use-package paredit)
(eval-after-load 'paredit
  ;; need a binding that works in the terminal
  '(define-key paredit-mode-map (kbd "M-)") 'paredit-forward-slurp-sexp))

(show-paren-mode 1)  ;; highlight matching parenthasis
(add-hook 'emacs-lisp-mode-hook 'paredit-mode)
(add-hook 'lisp-mode-hook 'paredit-mode)

;; nifty documentation at point for lisp files
(add-hook 'emacs-lisp-mode-hook 'turn-on-eldoc-mode)
(add-hook 'lisp-interaction-mode-hook 'turn-on-eldoc-mode)
(add-hook 'ielm-mode-hook 'turn-on-eldoc-mode)

(setq inferior-lisp-program "sbcl")

Slime is the big part of a lisp IDE. Its a process that runs an inferior process (usually a lisp interpreter) in the background and you can send information to it.

(use-package slime)

With lots of parens, it's easy to get lost. Fix that.

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

Python

There's a weird history with python and emacs. The FSF maintains a copy of python-mode which ships with emacs. The Python community maintains a separate version. They have evolved away from each other and each supports different things. I'm currently using the FSF version, but I'm not sold on it quite yet. I've run into a few syntax highlighting bugs where the buffer won't fully fill out. I'm now using python.el, as it works with yasnippet and indentation seems to be a bit better.

;; python
(add-hook 'python-mode-hook (lambda () 
                              ;; This breaks the blog export, as the
                              ;; python snippet doesn't actually have
                              ;; a filename. Need to investigate
                              ;; flycheck for options. We'll just
                              ;; spawn a new emacs without this
                              ;; enabled for now.
                              (setq fill-column 80)
                              (fci-mode 1)))

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

Virtualenv is a tool in the python community which sorts out your Python package dependencies into their own contained enviroments. This is similar to RVM and friends in the ruby community. I've moved away from virtualenvwrapper, preferring instead virtualenvs inside the relevant directory.

(use-package pyvenv)

Pony-mode is a Django helper mode which gives you access to many neat commands like runserver, manage, tests and more from handy keybindings. This is a small patch for the project which will take into account an directory which contains all of your apps and properly filter it out when determining app names.

(use-package pony-mode
  :config
  (setq pony-app-dir-prefix "apps")

  (defun pony-get-app ()
    "Return the name of the current app, or nil if no app
  found. Corrects for excluded prefix."
    (let* ((root (pony-project-root))
       (excluded-prefix (if (not (= (length pony-app-dir-prefix) 0)))
                    (concat root pony-app-dir-prefix "/")
                  root))
           (re (concat "^" (regexp-quote excluded-prefix) "\\([A-Za-z_]+\\)/"))
           (path (or buffer-file-name (expand-file-name default-directory))))
      (when (string-match re path)
        (match-string 1 path)))

  (defun pony-time ()
    "Helper function to get an immediate working setup after a reboot."
    (interactive)
    (if virtualenv-workon-session
        (progn
          (pony-runserver)
          (pony-manage-run '("celeryd" "-lINFO" "--traceback" "--autoreload"))
          (pony-shell)
          (sql-mysql))
      (error "setup your virtualenv first"))))

I rely on having a few utilities around in python land, so let's track a requirements file.

coverage
rope
ropemacs
-e git+https://github.com/offbyone/Pymacs@fix-python_load_helper-call#egg=Pymacs

Ropemacs is a binding to rope, which supports refactoring of Python code. It turns out that pymacs isn't super well maintained. A patch since 2013 still hasn't landed. We use offbyone's fork instead (which, unfortunately, isn't super well maintained either.). It all works though.

I need to fill out a weekly report of the sorts of stuff that I've been up to. This helps generate that using org-mode things.

(defun abrahms/weekly-report ()
  "What have I changed or added in the last 7 days?"
  (interactive)
  (let ((start (format-time-string "%Y-%m-%d" (time-add (current-time) (seconds-to-time (- (* 60 60 24 7))))))
        (end (format-time-string "%Y-%m-%d")))
    (org-ql-search
      (org-agenda-files)
      `(or
        (and (todo) (ts :from ,start :to ,end))
        (and (todo "DONE") (closed :from ,start :to ,end)))
      :sort '(todo reverse)
      )))

There was an additional step wherein I had to go to the checkout location of Pymacs and type make to have it compile the correct things. Rope also shadows my recompile key (C-c g), so disable that.

(ignore-errors
  (progn
    (require 'pymacs)
    (setq pymacs-python-command (expand-file-name "~/.virtualenvs/emacs/bin/python"))
    (pymacs-load "ropemacs" "rope-")
    (setq ropemacs-guess-project t)  ; don't prompt for project, try to figure it out.
    (setq ropemacs-enable-shortcuts nil) ; don't shadow C-c g for recompile.
    ;; Help found at http://stackoverflow.com/a/6806217/4972
    (eval-after-load "ropemacs-mode"
      (define-key ropemacs-local-keymap (kbd "C-c g") nil))))

Python-mode doesn't play well with electric-indent-mode. Instead of properly indenting things, it adds an extra level of indentation when you press RET. This is infuriating, so we turn it off.

;;; Indentation for python

;; Ignoring electric indentation
(defun electric-indent-ignore-python (char)
  "Ignore electric indentation for python-mode"
  (if (equal major-mode 'python-mode)
      'no-indent
    nil))
(add-hook 'electric-indent-functions 'electric-indent-ignore-python)

;; Enter key executes newline-and-indent
(defun set-newline-and-indent ()
  "Map the return key with `newline-and-indent'"
  (local-set-key (kbd "RET") 'newline-and-indent))
(add-hook 'python-mode-hook 'set-newline-and-indent)

I enjoy pdbpp, a tool which is an improvement to python's own pdb. The biggest feature for me is sticky mode, which shows you more context via curses. This config will enable it by default.

import pdb

class Config(pdb.DefaultConfig):
    sticky_by_default = True # start in sticky mode

Some of my projects use black, a code formatter for python. This sets it up.

(use-package python-black
  :after python
  :hook (python-mode . python-black-on-save-mode-enable-dwim))

Fancy Macros

(fset 'testify
   (lambda (&optional arg) "Converts test words into actual test functions.

  Converts something like `has token is 200` into `def
  test_has_token_is_200(self):\n\tpass` so I can easily type out my
  python test methods." (interactive "p") (kmacro-exec-ring-item (quote ([100 101 102 32 67108896 5 134217765 32 return 95 return 33 5 40 115 101 108 102 41 58 return 112 97 115 115 14 1 134217830 134217826] 0 "%d")) arg)))


Javascript / Typescript

Some generic javascript setup. There's a really neat thing called slime-js which I haven't setup yet. It allows you to have a slime process tied to a javascript REPL. The uptick of this is that you can also have that REPL tied to chrome's web inspector so the javascript you evaluate in it are also in the context of the currently opened webpage. I'm not yet sure how this will work in the context of our backbone app which uses closures everywhere, but we'll see.

(use-package js2-mode
  :mode ("\\.js" . js2-mode)
  :init
  (setq js2-global-externs '("it" "afterEach" "beforeEach" "before" "after" "describe" "require" "module"))

  ;; todo: I think js2-refactor-mode should go in it's own use-package?
  ;; :hook (js2-imenu-extras-mode
  ;;     add-node-modules-path
  ;;     js2-refactor-mode
  ;;     flycheck-mode)

  :config
  (setq-default js2-basic-offset 2)
  (setq js-indent-level 2))

(use-package json-mode
  :mode ("\\.json" . json-mode))


(use-package js2-refactor
  :requires js2-mode)

(use-package prettier-js
  :requires js2-mode)

(add-hook 'js2-mode-hook 'nvm-use-for-buffer)
(add-hook 'js2-mode-hook 'add-node-modules-path)
(add-hook 'js2-mode-hook 'prettier-js-mode)
;; (add-hook 'js2-mode-hook 'cov-mode)


;; TODO: It would be great to have flycheck enabled on json-mode to
;; detect json errors, but there's no hooks in the module today.

;; Javascript setup via https://emacs.cafe/emacs/javascript/setup/2017/04/23/emacs-setup-javascript.html
;; (js2r-add-keybindings-with-prefix "C-c C-r")

;; autocompletion via tern via https://emacs.cafe/emacs/javascript/setup/2017/05/09/emacs-setup-javascript-2.html

;; (require 'slime-js)

(defun find-imports (ext import-syntax-fn root tag)
  "Searches for occurrences of `tag` in files under `root` with extension `ext`

    Slightly confusing bash command which will search for java
    imports in your `get-java-project-root` directory and present you
    with a list of options sorted in most-used order. It does not
    insert them into the buffer, however.

    import-syntax-fn is a fn, given a tag, which returns an line of import code.

    returns a list of strings indicating used imports, most used first
    "


  (let* ((command (concat
                       ;;; find all java files in project root (excluding symlinks)
                   "find -P " root " -name '*." ext "' -type f | "
                       ;;; filter out imports that match tag
                   "xargs grep -h '" (funcall import-syntax-fn tag) "' "
                       ;;; group occurrences, count unique entries, then sort DESC
                   " | sort | uniq -c | sort -nr "
                       ;;; trim whitespace and ditch the count
                   " | sed 's/^\s*//' | cut -f2- -d ' '"))
         (results (shell-command-to-string command)))
    (progn
      (message command)
      (if (not (eq 0 (length results)))
          (split-string
           results
           "\n" t)))))

(defun copy-js-imports ()
  (interactive)
  (kill-new
   (first (find-imports "js" 
                        (lambda (tag) (concat tag " = require")) 
                        (textmate-project-root) (thing-at-point 'word)))))

A handy utility that will take a region and format is as JSON with nice indentation.

(defun pretty-print-json (&optional b e)
  "Shells out to Python to pretty print JSON" 
  (interactive "r")
  (shell-command-on-region b e "python -m json.tool" (current-buffer) t)
)

I attempted to setup javascript support within ctags using romainl/ctags-patterns-for-javascript, but it never actually worked for me, unfortunately. Perhaps worth trying again eventually.

Ctags happens to generate really large files. Let's disable the warning around opening large files (including TAGS files) unless its more than 50mb or so.

(setq large-file-warning-threshold 50000000) ;; 50mb

Keep ctags up to date.

(use-package ctags-update)

I use nvm for work which manages node versions and a node path. Teach emacs about that path. This auto-loads and there are no interactive functions, but it puts node on your path.

(use-package nvm
  :config
  (if (file-exists-p (expand-file-name "~/.nvm"))
      (let ((nvm-version "v14.18.1"))
        (condition-case nil
            (nvm-use nvm-version)
          (error (message "You tried to activate version %s, but it isn't installed" nvm-version))))))

We also want nodemodules on our path, because lots of juicy binaries live there.

(use-package add-node-modules-path
  :requires js2-mode)

I do a small amount of typescript at work.

(use-package typescript-mode
  :mode "\\.ts\\'"
  :init (setq typescript-indent-level 2))

golang

Go is nice enough to ship with a formatter. The least we could do is run it. Furthermore, there are helpful plugins around showing the method parameters via eldoc. This requires us to set up our exec-path to point to the gocode binary, which can be found here. Following along with gocode, that library provides an autocomplete setup, which I'd like to use.

;; based on the assumption that the following repos are installed and
;; available on exec-path.
;;
;; - github.com/nsf/gocode
;; - golang.org/x/tools/cmd/goimports
;; - github.com/rogpeppe/godef
;; - github.com/golang/lint

(use-package go-eldoc
  :requires go-mode
  :hook go-mode)

;; (use-package go-autocomplete)
(use-package gotest
  :hook go-mode
  :requires go-mode)

;; TODO(abrahms): maybe gopath isn't setup??
(use-package golint
  :requires go-mode
  :hook go-mode
  :init
  (add-to-list 'load-path (expand-file-name "~/src/golang.org/x/lint/misc/emacs")))

(use-package go-autocomplete
  :hook go-mode
  :requires go-mode
  :init
  ;; setting up autocomplete should happen after yasnippet so we don't duplciate tab bindings.
  (require 'auto-complete-config))

(use-package go-mode
  :commands go-mode
  :config
  (setq gofmt-command "goimports")
  (add-hook 'before-save-hook #'gofmt-before-save)
  (add-hook 'go-mode-hook 'go-eldoc-setup))

Jumping around in source code is quite helpful, but let's shadow normal ctags support.

(defun ja-gomode-hook ()
  (if not (string-match "go" compile-command)
    (set (make-local-variable 'compile-command)
         "go generate && go build -v && go test -v --coverprofile=cover.out && go vet"))
  (local-set-key (kbd "M-.") 'godef-jump))
(add-hook 'go-mode-hook 'ja-gomode-hook)

Other coding modes

(use-package protobuf-mode)
(use-package json-mode) ;; better syntax highlighting / error reporting for json
(use-package nginx-mode)
(use-package yaml-mode)
(use-package haskell-mode) ;; for xmonad configs
(use-package dockerfile-mode) ;; Dockerfiles
(use-package terraform-mode)
(use-package lua-mode)
(use-package nix-mode)

SQL

sql-postgres mode must defer passwords to a special, postgres-specific password file because postgres doesn't allow you to specify passwords on the command line (presumably due to security reasons). As such, we pass the -w flag to tell it to look for a password file.

(setq sql-postgres-options '("-P" "pager=off" "-w"))

Clojure

I've been playing a bit with clojure since folks at work use it.

(use-package clojure-mode)
(use-package cider)

Java

I programmed Java with Emacs at Google on and off for 2 years (swapping between Eclipse on occasion). Thanks to some awesome tools they have internally, it was pretty great. Similar to programming Python in emacs with an up-to-date TAGS file. I don't know that I'd do it outside of Google beyond a super tiny project, but the slowness of the custom eclipse plugin they had was just really difficult for me to cope with.

(add-hook 'java-mode-hook (lambda ()
                            (setq c-basic-offset 2)
                            (setq fill-column 100)
                            (fci-mode t)
                            (subword-mode t)
                            (local-set-key (kbd "C-M-h") 'windmove-left)
                            (hs-minor-mode 1)))

I have a lot of hope about lsp-java for making java in emacs a possibility, at least for small things like university coursework.

(use-package lsp-java
  :after lsp
  :hook (java-mode . lsp))

CSS / HTML authoring

CSS mode is pretty well done. Just change the indentation to 2 spaces rather than 4.

(setq css-indent-offset 2)

Linting is important in all languages, even CSS.

; (require 'flymake-less)
(use-package css-eldoc)

web-mode is an interesting new mode which bridges the gap with mixed-content template code. You get handy html syntax highlighting and basic controls, while simultaneously getting some help in the template code. This mostly manifests as control structures, pairing of open parens, etc.

(use-package web-mode
  :config

  (add-to-list 'auto-mode-alist '("\\.hb\\.html\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.phtml\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.tpl\\.php\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.jsp\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.as[cp]x\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.erb\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.html\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.hbs\\'" . web-mode))

  ;; everything is indented 2 spaces
  (setq web-mode-markup-indent-offset 2)
  (setq web-mode-css-indent-offset 2)
  (setq web-mode-code-indent-offset 2))

Setup various things for scss / less

(use-package scss-mode)
(use-package flymake-sass)
(use-package less-css-mode)

Easy html authoring tools

(use-package zencoding-mode)

Ruby

(use-package enh-ruby-mode) ; better ruby mode
(use-package inf-ruby) ; irb in separate pane
(use-package rspec-mode) ; ruby testing mode

GraphQL

(use-package graphql-mode)

Rust

Simple addition of the rust mode.

(use-package rustic
  :config
  (setq rustic-format-trigger 'on-compile)
  ;; stop rustfmt from stealing focus
  ;; via https://github.com/brotzeit/rustic/issues/519
  (setq rustic-format-display-method 'ignore))
(use-package cargo
  :defer t)

Add support for cargo check output in compilation mode.

;; source data for the regexp
;warning: unused import: `std::borrow::Cow`
;  --> rust/clusterdb/src/storage.rs:11:5
;   |
;11 | use std::borrow::Cow;
;   |     ^^^^^^^^^^^^^^^^
;   |
;   = note: `#[warn(unused_imports)]` on by default
;
(pushnew 'rust compilation-error-regexp-alist)
(pushnew '(rust "\\(error\\|warnin\\(g\\)\\).*\n.*--> \\([.A-Za-z/_-]*\\):\\([0-9]+\\):\\([0-9]+\\)"
                3 4 (2 . nil)) compilation-error-regexp-alist-alist)

Kubernetes

I've been poking around with k8s for some personal projects. I've heard wonderful thinks about kubernetes.el, so I figured I'd try it.

(use-package kubernetes)

PHP

(use-package php-mode
  :ensure t
  :config
  (lsp-mode t))

(require 'dap-php)
(setq dap-auto-configure-features '(sessions locals controls tooltip))
(add-hook 'dap-stopped-hook
          (lambda (arg) (call-interactively #'dap-hydra)))

Email

I've been using mu4e for both work and (more recently) personal email. These configs work with both exchange and my personal imap server.

mbsync

I've been using offlineimap for a while, but it suffers from two problems. First, it's a bit slow. Not critical, but a sticking point. More importantly, it's difficult to separate which things do or don't sync based on my particular hosts. I have work email and personal email. I'd love to have the same config and have my work email sync at work and personal email sync at home. My initial config was inspired by copyninja.

# Docs at https://isync.sourceforge.io/mbsync.html
SyncState *
Sync All

IMAPAccount fastmail
Host imap.fastmail.com
User justin@abrah.ms
PassCmd "gpg -q --for-your-eyes-only --no-tty --exit-on-status-write-error --batch --pinentry-mode loopback --passphrase-file ~/.gpg-pass.txt -d ~/.fastmail.password.gpg"
SSLType IMAPS
SSLVersion TLSv1.2
CertificateFile /etc/ssl/certs/ca-certificates.crt

IMAPStore fastmail-remote
Account fastmail

MaildirStore fastmail-local
SubFolders Verbatim
Path ~/Mail/fastmail/
Inbox ~/Mail/fastmail/INBOX


Channel fastmail
Far :fastmail-remote:
Near :fastmail-local:
Patterns * !migrated* !Spam
Create Both
SyncState *
Sync All

# -- work

IMAPAccount work
Host imap.gmail.com
User justin@subconscious.network
PassCmd "gpg -q --for-your-eyes-only --no-tty --exit-on-status-write-error --batch --pinentry-mode loopback  --passphrase-file ~/.gpg-pass.txt -d ~/.subconscious-email.password.gpg"
SSLType IMAPS
AuthMechs LOGIN

IMAPStore work-remote
Account work

MaildirStore work-local
SubFolders Verbatim
Path ~/Mail/work/
Inbox ~/Mail/work/INBOX

Channel work
Far :work-remote:
Near :work-local:
# Exclude everything under the internal [Gmail] folder, except the interesting folders
Patterns * !*Calendar* !Contacts !&XvqLrnaEgFR8+066-
Create Both

# -- School

IMAPAccount school
Host imap.gmail.com
User jabrahms@eou.edu
PassCmd "gpg -q --for-your-eyes-only --no-tty --exit-on-status-write-error --batch --pinentry-mode loopback --passphrase-file ~/.gpg-pass.txt -d ~/.eou.password.gpg"
SSLType IMAPS
AuthMechs LOGIN


IMAPStore school-remote
Account school

MaildirStore school-local
SubFolders Verbatim
Path ~/Mail/school/
Inbox ~/Mail/school/INBOX

Channel school
Far :school-remote:
Near :school-local:
# Exclude everything under the internal [Gmail] folder, except the interesting folders
Patterns * !*Calendar* !Contacts !&XvqLrnaEgFR8+066-
Create Both

Actually run the mail sync regularly

[Unit]
Description=Sync my mail regularly
Wants=mail-sync.service

[Timer]
Unit=mail-sync.service
# every 15m
OnCalendar=*:0/15

[Install]
WantedBy=timers.target
[Unit]
Description=Filter & sync the mail
Wants=mail-sync.timer

[Service]
Type=oneshot
ExecStart=/usr/bin/env bash -c "/home/abrahms/.nix-profile/bin/imapfilter && /home/abrahms/.nix-profile/bin/mbsync --all"

[Install]
WantedBy=default.target

To enable these, we need to run:

systemctl --user enable mail-sync.{timer,service}

Mu4e config

Mu4e isn't available via package.el, so we installed it from source.

(let ((mu4e-dir (shell-command-to-string "echo -n $(dirname $(dirname $(readlink -f $(which mu))))/share/emacs/site-lisp/mu4e")))
  (setq mu4e-installed (file-exists-p mu4e-dir))
  (setq mu4e-compose-dont-reply-to-self t)
  (if mu4e-installed
      (progn
        (add-to-list 'load-path mu4e-dir)
        (require 'mu4e)
        (global-set-key (kbd "C-x m") 'mu4e) ;; setup a mail shortcut.
        (require 'mu4e-org)

        (define-key mu4e-headers-mode-map (kbd "C-c c") 'mu4e-org-store-and-capture)
        (define-key mu4e-view-mode-map    (kbd "C-c c") 'mu4e-org-store-and-capture)

        ;; Without this changing of filenames, we get errors w/ UID
        ;; synchronization. Not vetted yet, but
        ;; https://stackoverflow.com/q/39513469/4972 is a breadcrumb.
        (setq mu4e-change-filenames-when-moving t)

        (require 'smtpmail)
        (setq mu4e-maildir "~/Mail")
        (setq mu4e-mu-binary "/home/abrahms/.nix-profile/bin/mu")

        (custom-set-variables
         '(mu4e-view-mode-hook '(turn-on-visual-line-mode)))

        (setq mail-user-agent 'mu4e-user-agent)
        (setq message-send-mail-function 'smtpmail-send-it)
        (set-variable 'read-mail-command 'mu4e)

        ;; enable viewing some messages in the browser if their html mode is useless.
        (add-to-list 'mu4e-view-actions
                     '("ViewInBrowser" . mu4e-action-view-in-browser) t)

        ;; via https://emacs.stackexchange.com/questions/51999/muting-threads-in-mu4e
        (add-to-list 'mu4e-headers-actions
                     '("Mute thread" .
                       (lambda (msg)
                         (let ((docid (mu4e-message-field msg :docid))
                               (refloc (if (stringp mu4e-refile-folder)
                                           mu4e-refile-folder
                                         (funcall mu4e-refile-folder msg))))
                           (mu4e-action-retag-message msg "+muted")
                           (mu4e~proc-move docid refloc "+S-N")))))

        (defun mu4e-archive-if-muted ()
          (let* ((cmd "mu find tag:muted --format=sexp -r")
                 (result (concat "(list " (shell-command-to-string cmd) ")"))
                 (msgs (car (read-from-string result))))
            (dolist (msg (cdr msgs))
              (let ((maildir (mu4e-message-field msg :maildir))
                    (docid (mu4e-message-field msg :docid))
                    (refloc (if (stringp mu4e-refile-folder)
                                mu4e-refile-folder
                              (funcall mu4e-refile-folder msg))))
                (unless (or (string= refloc maildir)
                            (string= "/sent" maildir))
                  (when (and docid (> docid 0))
                    (mu4e~proc-move docid refloc "+S-N")))))))

        (add-to-list 'mu4e-index-updated-hook 'mu4e-archive-if-muted t)

        (setq mu4e-bookmark-all-mail '(:name "All Inboxes" :query "(maildir:/fastmail/INBOX or maildir:/work/INBOX or maildir:/school/INBOX) and not flag:trashed" :key ?a))
        (setq mu4e-bookmarks
              `(,mu4e-bookmark-all-mail
                ("maildir:/fastmail/lists* NOT flag:trashed" "Mailing Lists" ?l)
                ("maildir:/Walmart/github AND NOT flag:trashed AND flag:unread" "Code Review" ?g)
                ("maildir:/Walmart/jira AND NOT flag:trashed AND flag:unread" "Tickets" ?j)
                (:name "Unread messages" :query "flag:unread AND NOT flag:trashed" :key ?u)
                (:name "Today's messages" :query "date:today..now" :key ?t)
                (:name "Last 7 days" :query "date:7d..now" :hide-unread t :key ?w)
                (:name "Messages with pictures" :query "mime:image/*" :key ?p)
                ))

        (setq user-full-name "Justin Abrahms")
        (setq smtpmail-servers-requiring-authorization ".*(gmail|fastmail).*")

        (setq mu4e-contexts `(
                              ,(make-mu4e-context
                                :name "Work"
                                :enter-func (lambda (&rest args)
                                              (setq mu4e-bookmarks
                                                    `(,mu4e-bookmark-all-mail
                                                      ("maildir:/work/INBOX AND NOT flag:trashed" "Inbox" ?i)
                                                      ))
                                              )
                                ;; leave-func not defined
                                :match-func (lambda (msg)
                                              (when msg
                                                (mu4e-message-contact-field-matches
                                                 msg :to ".*@subconscious.network")))
                                :vars '((user-mail-address . "justin@subconscious.network")
                                        (mu4e-sent-folder . "/work/[Gmail]/Sent Mail")
                                        (mu4e-drafts-folder . "/work/[Gmail]/Drafts")
                                        (mu4e-trash-folder . "/work/[Gmail]/Deleted Items")
                                        (mu4e-refile-folder . "/work/[Gmail]/Archive")
                                        (smtpmail-default-smtp-server . "smtp.gmail.com")
                                        (smtpmail-local-domain . "subconscious.network")
                                        (smtpmail-smtp-user . "justin@subconscious.network")
                                        (smtpmail-smtp-server . "smtp.gmail.com")
                                        (smtpmail-stream-type . starttls)
                                        (smtpmail-smtp-service . 587)
                                        )
                                )
                              ,(make-mu4e-context
                                :name "Fastmail"
                                ;; enter-func not defined
                                ;; leave-func not defined
                                :match-func (lambda (msg)
                                              (when msg
                                                (mu4e-message-contact-field-matches
                                                 msg :to ".*@abrah.ms")))
                                :vars '((user-mail-address . "justin@abrah.ms")
                                        (mu4e-sent-folder . "/fastmail/Sent")
                                        (mu4e-drafts-folder . "/fastmail/Drafts")
                                        (mu4e-trash-folder . "/fastmail/Trash")
                                        (mu4e-refile-folder . "/fastmail/Archive")
                                        (smtpmail-default-smtp-server . "smtp.fastmail.com")
                                        (smtpmail-local-domain . "abrah.ms")
                                        (smtpmail-smtp-user . "justin@abrah.ms")
                                        (smtpmail-smtp-server . "smtp.fastmail.com")
                                        (smtpmail-stream-type . starttls)
                                        (smtpmail-smtp-service . 587)))
                              ,(make-mu4e-context
                                :name "School"
                                :match-func (lambda (msg)
                                              (when msg
                                                (mu4e-message-contact-field-matches
                                                 msg :to ".*@eou.edu")))
                                :vars '((user-mail-address . "jabrahms@eou.edu")
                                        (mu4e-sent-folder . "/school/[Gmail]/Sent Mail")
                                        (mu4e-drafts-folder . "/school/[Gmail]/Drafts")
                                        (mu4e-trash-folder . "/school/[Gmail]/Trash")
                                        (mu4e-refile-folder . "/school/[Gmail]/All Mail")
                                        (smtpmail-default-smtp-server . "smtp.gmail.com")
                                        (smtpmail-local-domain . "eou.edu")
                                        (smtpmail-smtp-user . "jabrahms@eou.edu")
                                        (smtpmail-smtp-server . "smtp.gmail.com")
                                        (smtpmail-stream-type . starttls)
                                        (smtpmail-smtp-service . 587)
                                        ))
                              ))

        ;; press U to get updated email.
        (setq mu4e-get-mail-command "" ;; this is handled via cron
              send-mail-function 'smtpmail-send-it
              message-kill-buffer-on-exit t)

        ;; enable inline images
        (setq mu4e-view-show-images t)
        (setq mu4e-use-fancy-chars t)
        ;; use imagemagick, if available
        (when (fboundp 'imagemagick-register-types)
          (imagemagick-register-types))

        ;; for handling html emails
        (setq mu4e-html2text-command "w3m -T text/html")
        (setq mu4e-attachment-dir "~/Downloads/")

        ;; Prefer text, unless there's a 30x difference between plaintext and html length.
        (setq mu4e-view-html-plaintext-ratio-heuristic 30))))

Some emails are completely broken by the w3m handling above. Thankfully reddit user venkateshks came up with a bit of elisp which will open the message in chrome.

;;; message view action
(if mu4e-installed
    (progn
      (defun mu4e-msgv-action-view-in-browser (msg)
        "View the body of the message in a web browser."
        (interactive)
        (let ((html (mu4e-msg-field (mu4e-message-at-point t) :body-html))
              (tmpfile (format "%s/%d.html" temporary-file-directory (random))))
          (unless html
            (error "No html part for this message"))
          (with-temp-file tmpfile
            (insert "<html>" "<head><meta http-equiv=\"content-type\"" "content=\"text/html;charset=UTF-8\">" html))
          (browse-url (concat "file://" tmpfile))))
      (add-to-list 'mu4e-view-actions
                   '("View in browser" . mu4e-msgv-action-view-in-browser) t)))

ImapFilter

imapfilter is a tool which will run against your mail servers and move things around. From here, offlineimap will download them in the folders as they exist on the server. The configuration language is written in lua.

If you need to compile imapfilter on an ubuntu host, it looks like the stanza below. The LIBLUA overrides something in the Makefile because my system has liblua5.3.so not liblua.so.

LD_CONFIG=/usr/lib/x86_64-linux-gnu:$LD_CONFIG CPATH=/usr/include/lua5.3/:$INCLUDE_DIR make LIBLUA=-llua5.3 
(use-package lua-mode)
-- This supposedly fixes the EOF violation of the protocol SSL error I
-- get. via https://github.com/lefcha/imapfilter/issues/22
options.timeout = 60;
options.info = false

function hostname()
   local f = io.popen ("/usr/bin/env hostname")
   local hostname = f:read("*a") or ""
   f:close()
   hostname =string.gsub(hostname, "\n$", "")
   return hostname
end

function contains(haystack, needle)
   for i=1,#haystack do
      if haystack[i] == needle then
         return true
      end
   end
   return false
end


function main()
   local host = hostname()
   if host == "periwinkle" then
      walmart()
   elseif contains({"justin-x250","subconscious-nix"}, host) then
      fastmail()
   else
      print("Unknown host. Unsure which imap servers to contact")
   end
end

function charities(account, mbox)
      dst = account["charities"]
      mbox:contain_from("Mozilla@e.mozilla.org"):contain_subject("Receipt"):move_messages(dst) -- mozilla
      mbox:contain_subject("Recurring Contribution"):move_messages(dst) -- eff
      mbox:contain_subject('automatic payment to Wikimedia'):move_messages(dst) -- wikipedia
end

function fastmail()
   local fastmail = IMAP {
      server = "imap.fastmail.com",
      username = "justin@abrah.ms",
      password = read_encrypted_contents("~/.fastmail.password.gpg"),
      ssl = 'ssl3',
   }
   fastmail.INBOX:check_status()
   mails = fastmail.INBOX:select_all()

   delete_mail_if_from(mails, 'grubhub')
   delete_mail_if_from(mails, 'aetna')

   -- linkedin spam
   delete_mail_if_subject_contains(mails, 'add me to your LinkedIn network')
   delete_mail_if_subject_contains(mails, 'you\'re getting noticed')
   delete_mail_if_subject_contains(mails, 'invitation is waiting for your response')

   delete_mail_if_subject_contains(mails, 'sent a message to your group conversation') -- fb messenger
   delete_mail_if_subject_contains(mails, 'Terms of Service')
   delete_mail_if_subject_contains(mails, 'Change in Terms')
   delete_mail_if_subject_contains(mails, 'Your Statement of Account from')
   delete_mail_if_subject_contains(mails, 'has sent you a Direct Message on Twitter')

   -- receipts
   mails:match_subject('Your amazon.com order .* has shipped'):delete_messages()
   mails:match_subject('Your Electronic Receipt'):move_messages(fastmail["receipts"])
   mails:match_subject('Your Amazon.com order #'):move_messages(fastmail["receipts"])
   mails:match_subject('Mozilla VPN payment received'):move_messages(fastmail["receipts"])
   mails:match_subject('DNSImple Invoice'):move_messages(fastmail["receipts"])
   mails:match_subject('Billing Statement Available'):move_messages(fastmail["receipts"]) -- aws
   mails:match_subject('invoice is available'):move_messages(fastmail["receipts"]) -- digital ocean
   mails:match_subject('Statement Notification'):move_messages(fastmail["receipts"]) -- trash
   mails:match_subject('Your.*Reciept'):move_messages(fastmail["receipts"]) -- google play
   mails:match_subject('Thank you .* contribution'):move_messages(fastmail["receipts"]) -- open collective
   mails:contain_from('venmo@'):move_messages(fastmail["receipts"])

   charities(fastmail, mails)
   automatic_mailing_list_folders(fastmail, mails)
end

function walmart()
   local walmart = IMAP {
      server = read_encrypted('walmart', 'remotehost'),
      username = read_encrypted('walmart', 'username'),
      password = read_encrypted('walmart', 'password'),
      ssl = 'ssl3',
   }
   walmart.INBOX:check_status()

   mails = walmart.INBOX:select_all()
   delete_mail_if_from(mails, 'sneal@')
   delete_mail_if_to(mails, 'WalmartPayMetricsEma@')

   mails:contain_subject("Honesty Market"):delete_messages()
   mails:contain_subject("onboarded to Hygieia"):delete_messages()

   mails:contain_from('svc_automaton@walmartlabs.com'):move_messages(walmart['load-tests'])
   mails:contain_subject('Your meeting attendees are waiting'):delete_messages() -- zoom
   mails:contain_subject('has joined your Personal Meeting Room'):delete_messages() -- zoom

   mails:contain_subject('You have been onboarded to Hygieia'):delete_messages()


   -- I don't want to hear how everyone accepts my calendar invites
   mails:contain_subject('Accepted: '):delete_messages()
   mails:contain_subject('Tenative: '):delete_messages()
   mails:contain_subject('Meeting Forward Notification: '):delete_messages()

   -- people filing PTO requests
   mails:contain_subject('Walmart Pay Team Calendar'):delete_messages()

   -- Don't tell me about mail purge jobs
   walmart['L2-support']:contain_subject('Nightly Purge'):delete_messages()

   -- Journey mapping emails to a folder
   mails:contain_subject("FS Applications Journey Mapping"):move_messages(walmart['journey-mapping'])

   -- alerts that I don't care about
   mails:contain_from('support@oneops.com'):move_messages(walmart['ops-alerts'])

   eng_community = walmart.github:contain_subject('[engineering/community]')
   eng_community:contain_subject("] Release"):delete_messages()
end

function delete_mail_if_subject_contains(mails, subject)
    filtered = mails:contain_subject(subject)
    filtered:delete_messages()
end

function delete_mail_if_from(mails, from)
   filtered = mails:contain_from(from)
   filtered:delete_messages()
end

function delete_mail_if_to(mails, to)
   filtered = mails:contain_to(to)
   filtered:delete_messages()
end


function read_encrypted_contents(filename)
   local handle = io.popen("gpg -q --for-your-eyes-only --no-tty --exit-on-status-write-error --batch --passphrase-file ~/.gpg-pass.txt -d ".. filename)
   local result = handle:read("*a")
   handle:close()
   return result
end

-- Utility function to get IMAP password from a gpg encrypted file
function read_encrypted(key, value)
   local result = read_encrypted_contents("~/.offlineimappass.gpg")
   local obj, pos, err = json.decode (result, 1, nil)
   if err then
      print("Unable to parse json. ERR: " + err)
      return ""
   end
   return obj[key][value]
end

function automatic_mailing_list_folders(account, mbox)
      -- Thanks to KangOI at https://gist.github.com/KangOl/4533912
   messages = mbox:contain_field('List-Id', '')
   ML = "lists/"
   AUTO_ML = ML.."Auto/"
   mls = {}

   for _, message in ipairs(messages) do
      mailbox, uid = table.unpack(message)
      listid = mailbox[uid]:fetch_field('List-Id')
      subj = mailbox[uid]:fetch_field('subject')
      if bool(listid) then
         -- imapfilter bug: the search is also done inside the message body
         -- and thus also match the forwarded emails.
         s, m1, m2 = regex_search('^(?i)List-Id: (.*) <(.*)>$', listid)
         if s and bool(m1) and bool(m2) then
            mls[m2] = m1:gsub('"', '')
         end
      end
   end

   for listid, listtitle in pairs(mls) do
      dest = AUTO_ML..listtitle
      account:create_mailbox(dest)
      account:subscribe_mailbox(dest)
      messages:contain_field('List-Id', listid):move_messages(account[dest])
   end
end

bool = function(o)
    if not o then
        return false
    elseif o == '' then
        return false
    elseif o == 0 then
        return false
    elseif #o == 0 then
        return false
    end
    return true
end

pyor = function(o)
    if not bool(o) then
        return false
    end
    return o
end

main() -- execute main function from above

We also want to setup a crontab to fetch our mail every so often.

*/5 * * * * eval `luarocks path` && imapfilter && offlineimap

Handle mailto links

Thanks to this emacswiki entry, this script will open emacs and compose an email.

emacsclient -c --eval "(browse-url-mail \"$@\")"  

Calendaring

I'd like to get calendaring setup so that my exchange calendar dumps into an org-mode file. I suspect this will make my agenda mode easier to use and more accurate.

To use it, open up the calendar (M-x calendar) and press e to update.

(use-package excorporate)

(setq-default
 ;; configure email address and office 365 exchange server adddress for exchange web services
 excorporate-configuration
 (quote
  ("t0a01lc@homeoffice.wal-mart.com" . "https://outlook.wal-mart.com/EWS/Exchange.asmx"))
 ;; integrate emacs diary entries into org agenda
 org-agenda-include-diary t
 )

Emacs miscellany

Generic Emacs Stuff

Emacs has a few things that nearly everyone changes. Minimize the chrome (emacs should be as command-line-like as possible), lose the overly-verbose and annoying prompts, etc. These are all documented inline.

(defun load-if-exists (f)
  (if (file-exists-p (expand-file-name f))
      (load-file (expand-file-name f))))

(global-unset-key (kbd "s-t")) ;; disable osx font panel

(defalias 'qrr 'query-regexp-replace)
(fset 'yes-or-no-p 'y-or-n-p)  ;; only type `y` instead of `yes`
(setq inhibit-splash-screen t) ;; no splash screen
(setq-default indent-tabs-mode nil)      ;; no tabs!
(setq-default fill-column 80) ;; M-q should fill at 80 chars, not 75
(setq auth-sources '("~/.authinfo.gpg")) ;; Don't store plaintext passwords ever.

;; general programming things
;; other fonts I like: proggy clean, inconsolata, terminus.
(setq-default truncate-lines 1) ;; no wordwrap

(require 'uniquify)
(setq uniquify-buffer-name-style 'post-forward)  ;; buffernames that are foo<1>, foo<2> are hard to read. This makes them foo|dir  foo|otherdir
(setq desktop-load-locked-desktop "ask") ;; sometimes desktop is locked, ask if we want to load it.
(desktop-save-mode t) ;; auto-save buffer state on close for a later time.
(setq desktop-restore-eager 5) ;; don't load & render every file all at once. lazy load most of them in the background
(setq abbrev-file-name "~/.config/emacs/abbrev_defs") ;; where to save auto-replace maps

(use-package all-the-icons)
(use-package doom-modeline
  ;; note, if the fonts are weird, run `M-x all-the-icons-install-fonts`
  :init (doom-modeline-mode 1)) ;; enable pretty modeline

Zooming text is useful for presentations.

(defun djcb-zoom (n)
  "with positive N, increase the font size, otherwise decrease it"
  (set-face-attribute 'default (selected-frame) :height
                      (+ (face-attribute 'default :height) (* (if (> n 0) 1 -1) 10))))


(global-set-key (kbd "C-+")      '(lambda nil (interactive) (djcb-zoom 1)))
(global-set-key (kbd "C--")      '(lambda nil (interactive) (djcb-zoom -1)))

Below are a few kill-ring related changes for nice pasting. Pulled from this thread on pasting errors on linux.

(setq kill-ring-max 100)
(setq x-select-enable-clipboard t)
(setq select-active-regions t)
(setq save-interprogram-paste-before-kill 1)
(setq yank-pop-change-selection t)

My browser preference changes from time to time, but we tell emacs about the current flavor of the month here. This ensures when I open links with M-x browse-url that it opens in the correct browser..

;; Windows has a special, odd setup.
(if (eq window-system 'w32)
    (setq browse-url-browser-function 'browse-url-default-windows-browser)
  (setq
   browse-url-browser-function 'browse-url-generic
   browse-url-generic-program "firefox"))

Some variables are file-specific and are denoted in a header function. I allow those in particular so that loading them in daemon mode doesn't prompt me. The mechanism here is to provide a callable that checks the value of that variable.

(put 'encoding 'safe-local-variable (lambda (val) #'stringp))
(put 'org-src-preserve-indentation 'safe-local-variable (lambda (val) #'booleanp))

Make the emacs built-in helpfiles more helpful.

(use-package helpful
  :custom
  (counsel-describe-function-function #'helpful-callable)
  (counsel-describe-variable-function #'helpful-variable)
  :bind
  ([remap describe-function] . helpful-function)
  ([remap describe-symbol] . helpful-symbol)
  ([remap describe-variable] . helpful-variable)
  ([remap describe-command] . helpful-command)
  ([remap describe-key] . helpful-key))

;; ensure the helpful package can find the associated c source code.
(setq source-directory (expand-file-name "~/src/emacs/"))

Ensure buffers are saved when we're not paying attention. Git will ensure we don't lose things.

(use-package super-save
  :defer 1
  :diminish super-save-mode
  :config
  (super-save-mode +1)
  (setq super-save-auto-save-when-idle t))

Helpful elisp things.

There's no clear support for adding multiple things to a list via add-to-list, so this does that.

(defun ja/add-to-list-multiple (list to-add)
  "Adds multiple items to LIST.
Allows for adding a sequence of items to the same list, rather
than having to call `add-to-list' multiple times."
  (interactive)
  (dolist (item to-add)
    (add-to-list list item)))

Emacs Built-in configuration

tramp

Tramp is one of those features that you don't really make use of in the beginning, but as you get more familiar with it, the more indespensible it is. Tramp allows you to edit files on remote servers as if they were on your local machine. From the find-file prompt, you can type things like: /ssh:user@host:/home/user/myfile.txt which will ssh in to host as user and open up myfile.txt in emacs. When you save, changes are pushed back to the remote host. You can also edit files as root (I do it via sudo) like /sudo:host:/etc/nginx/nginx.conf

If I access something via root@host, actually ssh into the service using my default username (which is the username of my current system user) and sudo to root. I disable root access on my servers (Ubuntu default) which stops a reasonable number of attacks.

(use-package tramp
  :config
  ;; NB: I had to remove this b/c I couldn't do /sudo::/ on my localhost if ssh wasn't setup
  ; if I use tramp to access /ssh:root@..., then actually ssh into it
  ; and sudo, not login as root.
  ;; (set-default 'tramp-default-proxies-alist (quote ((".*" "\\`root\\'" "/sudo:%h:"))))
  )

server-mode

Emacs has this really interesting feature called server-mode. Emacs is notoriously slow to start (this happens if you have a giant emacs config that does stupid things). To combat this, you can start a single server process which will accept multiple clients. The server maintains the state of everything (files open, variables defined, processes running) and your client can attach / disconnect as necessary. The connecting is super fast (vim speeds).

(use-package server
  :defer 1
  :config
  (unless (server-running-p)
    (server-start)))

encryption mode

I keep a file around of encrypted passwords that emacs needs to know about (simple stuff like my znc-password to connect to my IRC server). I store that in a gpg encrypted file. Thankfully, emacs has nifty ways of building that stuff in.

(require 'epa)
(epa-file-enable)
(setq epg-gpg-program "gpg")
;; loopback makes it ask me for my passphrase
(setq epa-pinentry-mode 'loopback)


(load-if-exists "~/.config/emacs/secrets.el.gpg")
(load-if-exists "~/.config/emacs/secrets.el")

ERC

ERC is an IRC mode for emacs. Its nothing special. ZNC is a plugin which makes it simpler to connect to a ZNC server. ZNC is an IRC bouncer, which is a long-running process which keeps you on IRC. You can join and quit as you like, but you stay online throughout. Very similar to emacs's server-mode. Thanks to @bitprophet for letting me use his ZNC server.

(use-package znc
  :if (boundp 'znc-password)
  :init
  (setq znc-servers
        `(("justin.abrah.ms" 5000 t
           ((freenode "justinabrahms" ,znc-password)))))
  :config
  (setq erc-current-nick-highlight-type 'all)
  (setq erc-keywords '("jlilly"))
  ;; by default, erc alerts you on any activity. I only want to hear
  ;; about mentions of nick or keyword
  (setq erc-track-exclude-types '("JOIN" "PART" "NICK" "MODE" "QUIT"))
  (setq erc-track-use-faces t)
  (setq erc-track-faces-priority-list
        '(erc-current-nick-face erc-keyword-face))
  (setq erc-track-priority-faces-only 'all))

ibuffer

Having lots of buffers is a pretty common occurance in emacs, especially with a long-lived emacs process thanks to server-mode. As I'm writing this, I have 616 buffers open in emacs. Managing all that is difficult without some really helpful tools. ido-mode gets most of the way there as I can fuzzy find buffers based on their filename (and parent directories in the case of duplicates). For other times, I turn to ibuffer which presents a list of buffers. You can group these based on several parameters. I tend to do it based on project path or major mode.

(use-package ibuffer-vc
  :config
  (add-hook 'ibuffer-hook
    (lambda ()
      (ibuffer-vc-set-filter-groups-by-vc-root)
      (unless (eq ibuffer-sorting-mode 'alphabetic)
        (ibuffer-do-sort-by-alphabetic)))))


 ;;;###autoload
(defun ibuffer-set-filter-groups-by-path ()
  "Set the current filter groups to filter by file path."
  (interactive)
  (setq ibuffer-filter-groups
        (mapcar 'ibuffer-header-for-file-path
                (let ((paths (ibuffer-remove-duplicates
                              (mapcar 'buffer-file-name (buffer-list)))))
                  (if ibuffer-view-ibuffer
                      paths))))
  (ibuffer-update nil t))



(defun ibuffer-set-filter-groups-by-mode ()
  "Set the current filter groups to filter by mode."
  (interactive)
  (setq ibuffer-filter-groups
        (mapcar (lambda (mode)
                  (cons (format "%s" mode) `((mode . ,mode))))
                (let ((modes
                       (ibuffer-remove-duplicates
                        (mapcar (lambda (buf)
                                  (buffer-local-value 'major-mode buf))
                                (buffer-list)))))
                  (if ibuffer-view-ibuffer
                      modes
                    (delq 'ibuffer-mode modes)))))
  (ibuffer-update nil t))

It's also nice when you can read the size column. Let's make it human readable.

;; Use human readable Size column instead of original one
(define-ibuffer-column size-h
  (:name "Size" :inline t)
  (cond
   ((> (buffer-size) 1000000) (format "%7.1fM" (/ (buffer-size) 1000000.0)))
   ((> (buffer-size) 100000) (format "%7.0fk" (/ (buffer-size) 1000.0)))
   ((> (buffer-size) 1000) (format "%7.1fk" (/ (buffer-size) 1000.0)))
   (t (format "%8d" (buffer-size)))))

;; Modify the default ibuffer-formats
  (setq ibuffer-formats
        '((mark modified read-only " "
                (name 18 18 :left :elide)
                " "
                (size-h 9 -1 :right)
                " "
                (mode 16 16 :left :elide)
                " "
                filename-and-process)))

Dedicated Mode

Dedicated mode fixes the issue in which emacs spawns a new window (for tab completion or help, for instance) and it replaces an existing buffer you had open which you wanted to be persistent. If you turn on the dedicated minor-mode, none of those transient buffers will open up over those buffers.

(use-package dedicated) ;; sticky windows

Keybindings

Just a few custom keybindings I have. The big ones here are my window moving commands. The emacs default is C-x o which will progress through the windows in some semi-sane order one at a time. What I find myself actually wanting is something akin to vim movement commands. The unfortunate situation is that the key-bindings I'm using aren't in the space of keybindings reserved for users to override. This has the unfortunate side effect of meaning that I need to override it in a half a dozen different modes. I'm still looking for a better solution. I think it might be to use the super key which is still reserved but less likely to be used.

;; Vim style keyboard moving
(global-set-key (kbd "C-M-l") 'windmove-right)
(global-set-key (kbd "C-M-h") 'windmove-left)
(global-set-key (kbd "C-M-j") 'windmove-down)
(global-set-key (kbd "C-M-k") 'windmove-up)
(global-set-key (kbd "M-o") 'other-window)
(global-set-key (kbd "C-c g") 'recompile)
(global-unset-key (kbd "C-z")) ; suspending frame is useless with emacsclient and/or tmux
(add-hook 'perl-mode-hook (lambda ()
                            (local-set-key (kbd "C-M-h") 'windmove-left)))
(add-hook 'ruby-mode-hook (lambda ()
                            (local-set-key (kbd "C-M-h") 'windmove-left)))
(add-hook 'c-mode-common-hook (lambda ()
                                (local-set-key (kbd "C-M-h") 'windmove-left)))

(global-set-key (kbd "M-j") (lambda ()
                              (interactive)
                              (join-line -1)))

Platform Hacks

Using Emacs from within the terminal in OSX or WSL completely breaks copy+paste support. Using xcilp should restore it.

(use-package xclip)
(if (executable-find "wsl.exe")
    (progn
      (customize-set-variable 'xclip-method 'powershell)
      (customize-set-variable 'xclip-program "clip.exe"))
  )

Host specific overrides

For some hosts (such as my work machine), I want to change up the config a little. That happens here.

(load-if-exists (concat "~/" (system-name) ".el"))

Magit

Magit requires emacs 24.4. We'll validate the version is at least that before installing it.

From this Stack Overflow question, this snippet of code advises makes it so magit commits always show the verbose commit stuff when you're writing your commit message. This is helpful to me because I often forget what it is that I'm committing and need a simple refresher.

(if (emacs-version-gt-p 24 4)
    (use-package magit
      :bind (("C-x g" . 'magit-status-here))

      :init
      (advice-add #'magit-key-mode-popup-committing :after
                (lambda ()
                  (magit-key-mode-toggle-option (quote committing) "--verbose")))))

There's a comprehensive approach to getting access to pull requests and such from within emacs called "Forge". This sets that up to load after magit.

(if (emacs-version-gt-p 24 4)
    (use-package forge
      :after magit
      :config
      (add-to-list 'forge-alist '("gecgithub01.walmart.com" "gecgithub01.walmart.com/api/v3" "walmart" forge-github-repository))))

It's also quite nice to be able to open the current point on your repo host. This package solves that.

(use-package browse-at-remote)
(use-package git-link) ;; link to a given line of code you're on

deadgrep

Deadgrep is a tool which makes searching easier and faster. It uses ripgrep under the hood, which ignores things like .git directories and obeys your .gitignore. It's like ack or ag, but faster.

Because old habits die hard, alias find-grep to deadgrep.

(use-package deadgrep)

(defalias 'find-grep 'deadgrep)

Flycheck

Flycheck does syntax checking for your buffer. We install it with use-package.

(use-package flycheck
  :defer t
  :hook ((lsp-mode . flycheck-mode)
         (sh-mode . flycheck-mode)
         (go-mode . flycheck-mode)
         (python-mode . flycheck-mode)
         (markdown-mode . flycheck-mode)
         (js2-mode . flycheck-mode)))

Viewers

RFC

I've found when reading along with the in the gemini spec, that being able to pull up RFCs is of use.

(use-package rfc-mode
  :config (setq rfc-mode-directory (expand-file-name "~/src/rfc-editor.org/")))

PDF viewing

(use-package pdf-tools
  ;; Unsure how to make this depend on libraries, but should get an
  ;; answer at:
  ;; https://github.com/waymondo/use-package-ensure-system-package/issues/2
  ;; libpng-dev zlib1g-dev libpoppler-glib-dev libpoppler-private-dev imagemagick
  :init (pdf-tools-install)
  :config (setq-default pdf-view-display-size 'fit-width)

  )

Package maintenance

I'm beginning to make small elisp packages and these packages help me ensure that they're of high quality.

(use-package package-lint)

Rarely used

These are packages which were once used more than they are now, but I feel compelled to keep them around for some reason.

scpaste

SCPaste is sort of like gists, but it uploads the paste to your own server. It was particularly helpful when dealing with things at Google when I couldn't post it publically (or even privately to an external service). One of the neat things it does is it uses your color scheme (if you use a colored emacs) in the paste.

(use-package scpaste
  :config
  (setq scpaste-http-destination "http://justin.abrah.ms/pastes"
        scpaste-scp-destination "justin.abrah.ms:/srv/justin.abrah.ms/pastes"))

Beancounter

I track my finances using Martin Blais's beancount program. It's a little neckbeardy, but I like the flexibility it offers and the option to write small python scripts against my finances. Because it runs on python3, my virtualenv setup isn't quite there to support it. As such, we'll manually set the path to the bean-check file which does the linting of my balance sheets.

(setq beancount-check-program (expand-file-name "~/.virtualenvs3/beancount/bin/bean-check"))

Webjump

Emacs has a feature which lets you quickly jump to a website from your terminal. Helpful blog post which turned me on to it.

(setq webjump-sites '(
 ("Emacs Wiki" .
  [simple-query "www.emacswiki.org" "www.emacswiki.org/cgi-bin/wiki/" ""])
 ("search" .
  [simple-query "duckduckgo.com" "duckduckgo.com/?q=" ""])
 ("Wikipedia" .
  [simple-query "wikipedia.org" "wikipedia.org/wiki/" ""])
 ("Weather" . webjump-to-iwin)))

elpher

I've been playing with gemini (and to a lesser extent, the gopher protocol), an alternative to the web. It's got a ton of great content on it. It's useful to have a client to browse it.

(use-package elpher)

Snippets

It's quite nice to have tab completing expansion similar to textmate snippets. There's a thing that does this in emacs called yasnippets.

(use-package yasnippet
  :config
  (yas-global-mode 1)
  (if (file-exists-p (expand-file-name "~/src/github.com/dominikh/yasnippet-go"))
      (add-to-list 'yas-snippet-dirs (expand-file-name "~/src/github.com/dominikh/yasnippet-go/"))))

We can set up some snippets that are based on specific modes.

# key: pdb
# name: Debugging Statement
# contributor: Justin Abrahms <justin@abrah.ms>
# --
import pdb; pdb.set_trace()

Interactive Shell prompts

A few configurations and custom defined shell methods for eshell. Eshell is a terminal replacement implemented entirely in elisp. This sounds weird. It is weird. It has the benefit of having elisp as a first class language so you can do things like: cat foo/bar/baz > (switch-to-buffer "*test*") which opens the file contents in a new buffer names *test*.

  (setq path-to-etags "/usr/bin/etags")

(with-eval-after-load 'esh-opt
  (setq eshell-destroy-buffer-when-process-dies t)
  (setq eshell-visual-commands '("htop" "zsh" "vim")))


  ;; if OSX...
  (if (equal window-system 'ns)
      (progn
        (push "/Applications/Emacs.app/Contents/MacOS/bin" exec-path)
        (setq path-to-etags "/Applications/Emacs.app/Contents/MacOS/bin/etags")))

  (defun if-string-match-then-result (to-match pairs)
    "Takes a string to match and a list of pairs, the first element
    of the pairs is a regexp to test against the string, the second of
    which is a return value if it matches."
    (catch 'break
      (dolist (val pairs)
        (if (string-match-p (car val) to-match)
            (progn
              (throw 'break (cadr val)))))
      (throw 'break nil)))

  (setq eshell-history-size nil) ;; sets it to $HISTSIZE

  (defun eshell/extract (file)
    (eshell-command-result (concat (if-string-match-then-result
                                    file
                                    '((".*\.tar.bz2" "tar xjf")
                                      (".*\.tar.gz" "tar xzf")
                                      (".*\.bz2" "bunzip2")
                                      (".*\.rar" "unrar x")
                                      (".*\.gz" "gunzip")
                                      (".*\.tar" "tar xf")
                                      (".*\.tbz2" "tar xjf")
                                      (".*\.tgz" "tar xzf")
                                      (".*\.zip" "unzip")
                                      (".*\.jar" "unzip")
                                      (".*\.Z" "uncompress")
                                      (".*" "echo 'Could not extract the requested file:'")))
                                   " " file)))

  (defun mass-create-eshells (names)
    "Creates several eshells at once with the provided names. Names
    are surrounded in astrisks."
    (dolist (name names)
      (let ((eshell-buffer-name (concat "*" name "*")))
        (eshell))))

  (defun eshell/clear ()
    "clear the eshell buffer."
    (interactive)
    (let ((inhibit-read-only t))
      (erase-buffer)))

  (defun eshell/mcd (dir)
    "make a directory and cd into it"
    (interactive)
    (eshell/mkdir "-p" dir)
    (eshell/cd dir))

  (defun eshell/git-delete-unreachable-remotes ()
    "Delete remote git branches which have been merged into master"
    (interactive)
    (if (not (string-equal "master" (magit-get-current-branch)))
        (message "Not on master. This probably doesn't do what you want."))
    (shell-command "git branch -r --merged | grep -v '/master$' | sed -E 's/origin\\/(.*)/:\\1/' | xargs git push origin"))

Eshell prompts can get a bit long. This shortens them.

(defun abrahmsj/eshell--shorter-path (path max-len)
  "Return a potentially trimmed-down version of the directory PATH, replacing
parent directories with their initial characters to try to get the character
length of PATH (sans directory slashes) down to MAX-LEN."
  (let* ((components (split-string (abbreviate-file-name path) "/"))
         (len (+ (1- (length components))
                 (cl-reduce '+ components :key 'length)))
         (str ""))
    (while (and (> len max-len)
                (cdr components))
      (setq str (concat str
                        (cond ((= 0 (length (car components))) "/")
                              ((= 1 (length (car components)))
                               (concat (car components) "/"))
                              (t
                               (if (string= "."
                                            (string (elt (car components) 0)))
                                   (concat (substring (car components) 0 2)
                                           "/")
                                 (string (elt (car components) 0) ?/)))))
            len (- len (1- (length (car components))))
            components (cdr components)))
    (concat str (cl-reduce (lambda (a b) (concat a "/" b)) components))))

(defun abrahmsj-eshell-prompt-function ()
  (concat (abrahmsj/eshell--shorter-path (eshell/pwd) 40)
          (if (= (user-uid) 0) " # " " $ ")))

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

This allows me to swap the eshell buffer to the directory of my current buffer.

(defun eshell-cwd ()
  "
  Sets the eshell directory to the current buffer

  Usage: M-x eshell-cwd
  "
  (interactive)

  (let (
        (path (file-name-directory (or  (buffer-file-name) default-directory)))
       )
    (progn
      (with-current-buffer "*eshell*"
        (cd path)
        (eshell-emit-prompt))
      (switch-to-buffer "*eshell*"))))

And this allows for setting a PAGER that's valid for use within eshell. Otherwise, you get a prompt about how eshell isn't a good enough terminal to run less.

;; Taken from https://www.reddit.com/r/emacs/comments/8z9fp8/how_so_you_use_eshell/e2hprla/
;; https://github.com/cadadr/configuration/blob/master/emacs.d/extras/eless.sh
;; https://github.com/cadadr/configuration/blob/5b4f17d140728236056dde4b1f0c7f139fd10b94/emacs.d/init.el#L668

(defun gk-less (fifo)
  "Companion function for ‘extras/eless.sh’."
  (let ((buf (generate-new-buffer "*pager*")))
    (make-process
     :name "gk-pager" :buffer buf :command `("cat" ,fifo)
     :sentinel #'gk-less--proc-sentinel
     :filter #'gk-less--proc-filter)
    (view-buffer buf 'kill-buffer)))

(defun gk-less--proc-sentinel (proc string)
  (ignore proc string))

(defun gk-less--postprocess (proc)
  (goto-char (point-min))
  (cond
   ;; Man pages:
   ((save-excursion (search-forward "" nil t))
    (Man-fontify-manpage))
   ;; Diffs:
   ((save-excursion
      (and (looking-at "^diff")
           (re-search-forward "^---" nil t)
           (re-search-forward "^@@" nil t)))
    (diff-mode))
   (:else
    (special-mode))))

(defun gk-less--proc-filter (proc string)
  (let ((buf (process-buffer proc))
        (mark (process-mark proc)))
    (with-current-buffer buf
      (let ((buffer-read-only nil))
        ;; make sure point stays at top of window while process output
        ;; accumulates
        (save-excursion
          (goto-char mark)
          (insert string)
          (ansi-color-filter-region mark (point))
          (set-marker mark (point)))
        ;; Post-processing the buffer:
        (unless (process-live-p proc)
          (gk-less--postprocess proc))))))

(setenv "PAGER" "eless.sh")

And the shell script that makes it work.

# A script for $PAGER that redirects to an Emacs buffer
# Adapted from https://crowding.github.io/blog/2014/08/16/replace-less-with-emacs/

set -e

# make a named fifo
FIFO=$(mktemp -ut pagerXXXXXXXXXXX.$$)
mkfifo $FIFO

emacsclient -u -e "(gk-less \"$FIFO\")"

exec cat > "$FIFO"

When eshell isn't quite the thing I want, vterm allows me to use what feels like a real terminal within emacs.

  ;; improve colors in terminals
  (use-package eterm-256color
    :hook (term-mode))

  (use-package vterm
    ;; Note this requires cmake 3.11. Ubuntu 18.04 only has 3.10.
    ;; Install it via https://askubuntu.com/a/1157132/1105 if you must
    :config '(
              (setq vterm-always-compile-module t)
              (setq vterm-term-environment-variable "eterm-256color")
              (setq vterm-kill-buffer-on-exit t)))

(use-package eshell-syntax-highlighting
  :after esh-mode
  :config
  (eshell-syntax-highlighting-global-mode +1))

System-wide emacs popups

Protesilaos Stavrou had a wonderful post about creating global shortcuts which will pop up an emacs frame with some given command (like org-capture).

;;;; Run commands in a popup frame
(defun prot-window-delete-popup-frame (&rest _)
  "Kill selected selected frame if it has parameter `prot-window-popup-frame'.
Use this function via a hook."
  (when (frame-parameter nil 'prot-window-popup-frame)
    (delete-frame)))

(defmacro prot-window-define-with-popup-frame (command)
  "Define interactive function which calls COMMAND in a new frame.
Make the new frame have the `prot-window-popup-frame' parameter."
  `(defun ,(intern (format "prot-window-popup-%s" command)) ()
     ,(format "Run `%s' in a popup frame with `prot-window-popup-frame' parameter.
Also see `prot-window-delete-popup-frame'." command)
     (interactive)
     (let ((frame (make-frame '((prot-window-popup-frame . t)))))
       (select-frame frame)
       (switch-to-buffer " prot-window-hidden-buffer-for-popup-frame")
       (condition-case nil
           (call-interactively ',command)
         ((quit error user-error)
          (delete-frame frame))))))

And then configure specific functions we'd like to be able to call.

(declare-function org-capture "org-capture" (&optional goto keys))

;; This is defined in org-capture. I believe we re-define it here so that we
;; don't need to have the variable in scope(?)
(defvar org-capture-after-finalize-hook)

;; once the capture is done, close the frame.
(add-hook 'org-capture-after-finalize-hook #'prot-window-delete-popup-frame)

;; this defines the "prot-window-popup-org-capture" function
(prot-window-define-with-popup-frame org-capture)

For OSX, I had to use automator to create a "quick action" with "no input in any application". It had a single "Run Shell Script" action with this for contents:

/Applications/Emacs.app/Contents/MacOS/bin/emacsclient -e '(progn (x-focus-frame nil) (prot-window-popup-org-capture))'

and then bind it under Settings > Keyboard > Keyboard Shortcuts > Services > General > org-capture.

Undocumented

These are things, for whatever reason, I haven't had a chance to document. Some of it, I forgot why I added it, but assume it was for a reason (I already feel ashamed. Let's not talk about it.) Others are temporary. The rest are so small, I didn't have much to say about them.

(require 'subr-x) ;; random extra utils like hash maps

;; This is a terrible idea and makes everything wrap poorly, including source code.
;; (global-visual-fill-column-mode) ;; always enable visual-fill-mode
(use-package visual-fill-column) ;; wrap text nicely.


(defun create-tags (dir-name)
  "Create tags file."
  (interactive "DDirectory: ")
  (shell-command
   (format "find %s -type f | xargs %s -a -o %s/TAGS" dir-name path-to-etags dir-name)))

(setq auto-mode-alist ;; files called .bashrc should be opened in sh-mode
      (append
       '(("\\.bashrc" . sh-mode))
       '(("Vagrantfile" . ruby-mode))
       auto-mode-alist))

;; tempfiles, stolen from github://defunkt/emacs
(defvar user-temporary-file-directory
  (concat temporary-file-directory user-login-name "/"))
(make-directory user-temporary-file-directory t)
(setq backup-by-copying t
      backup-directory-alist `(("." . ,user-temporary-file-directory))
      auto-save-list-file-prefix (concat user-temporary-file-directory ".auto-saves-")
      auto-save-file-name-transforms `((".*" ,user-temporary-file-directory)))


;; auto-revert Dired and other buffers
(require 'dired-x)
(add-hook 'dired-mode-hook (lambda ()
                             (dired-omit-mode 1)))
(setq global-auto-revert-non-file-buffers t)

(use-package dired-single) ; keep dired stuff in a single window instead of a bunch of them


;; Fantastic dired thing which will hide most `ls -l` output.
;; ) and ( to toggle it
;; (require 'dired-details)
;; (dired-details-install)

;; scala

;; minibuffer command history
(setq savehist-additional-variables    ;; also save...
      '(search-ring regexp-search-ring)    ;; ... my search entries
      savehist-file "~/.config/emacs/savehist") ;; keep my home clean
(savehist-mode t)                      ;; do customization before activate

(defun jump-to-next-char (c &optional count)
  "Jump forward or backward to a specific character.  With a
  count, move that many copies of the character."
  (interactive "cchar: \np")
  (when (string= (string c) (buffer-substring (point) (+ 1 (point))))
    (setq count (+ 1 count)))
  (and
   (search-forward (string c) nil t count)
   (> count 0)
   (backward-char)))
(global-set-key (kbd "C-:") 'jump-to-next-char)


;; turning on autofill everywhere seems to give errors like "error in
;; process filter: Wrong type argument: stringp, nil" and other randomness.
(remove-hook 'text-mode-hook 'turn-on-auto-fill)

(put 'upcase-region 'disabled nil)
(put 'downcase-region 'disabled nil)
(put 'set-goal-column 'disabled nil)
(put 'narrow-to-region 'disabled nil)

(custom-set-variables
 ;; custom-set-variables was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 '(custom-safe-themes (quote ("870a63a25a2756074e53e5ee28f3f890332ddc21f9e87d583c5387285e882099" "159bb8f86836ea30261ece64ac695dc490e871d57107016c09f286146f0dae64" "5e1d1564b6a2435a2054aa345e81c89539a72c4cad8536cfe02583e0b7d5e2fa" "211bb9b24001d066a646809727efb9c9a2665c270c753aa125bace5e899cb523" "5727ad01be0a0d371f6e26c72f2ef2bafdc483063de26c88eaceea0674deb3d9" "30fe7e72186c728bd7c3e1b8d67bc10b846119c45a0f35c972ed427c45bacc19" default)))
 '(display-time-mode t)
 '(elisp-cache-byte-compile-files nil)
 '(erc-truncate-mode t)
 '(google-imports-file-for-tag (quote (("ServiceException" . "javax.xml.rpc.ServiceException") ("MalformedURLException" . "java.net.MalformedURLException") ("URL" . "java.net.URL") ("Named" . "com.google.inject.name.Named") ("Inject" . "com.google.inject.Inject") ("FormattingLogger" . "java/com/google/common/logging/FormattingLogger.java"))))
 '(menu-bar-mode nil)
 '(minibuffer-prompt-properties (quote (read-only t point-entered minibuffer-avoid-prompt face minibuffer-prompt)))
 '(safe-local-variable-values (quote ((virtualenv-default-directory . "/Users/justinlilly/src/prbot/") (virtualenv-workon . "prbot") (Mode . js))))
 '(tool-bar-mode nil))


Output the source of mscgen-mode which isn't loadble anywhere else via melpa. Source.

;;; mscgen-mode.el --- Major mode for editing mscgen sequence diagrams

;; Copyright (C) 2018 Thomas Stenersen

;; Author: Thomas Stenersen <stenersen.thomas@gmail.com>
;; Version: 0.1

;; This file is not part of GNU Emacs.

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; Major mode for editing mscgen sequence diagrams

;;; Code:


(defgroup mscgen nil "mscgen customizations"
  :group 'languages)

(defcustom mscgen-executable
  "mscgen"
  "Path to `mscgen' exectuable."
  :type 'string
  :group 'mscgen)

(defcustom mscgen-output-file-type
  "png"
  "Default output file type."
  :group 'mscgen
  :type 'string
  :options '("png" "eps" "svg" "ismap"))

;;; Fontification

(defconst mscgen--keywords
  '("label" "URL" "ID" "IDURL" "arcskip" "linecolor" "textcolor" "textbgcolor"
    "arclinecolor" "arctextcolor" "arctextbgcolor"))

(defconst mscgen--keywords-extra
  '("linecolour" "textcolour" "textbgcolour" "arclinecolour" "arctextcolour"
    "arctextbgcolour"))

(defconst mscgen--types
  '("box" "note" "rbox" "abox" "->" "<-" "=>" "<=" ">>" "<<"
    "=>>" "<<=" ":>" "<:" "-x" "x-" "*<-" "->*" "..." "---"
    "|||"))

(defconst mscgen--functions
  '("arcgradient" "wordwraparcs" "width" "hscale"))

(defconst mscgen--font-lock-keywords
  (let* ((x-keywords-regexp (regexp-opt (append mscgen--keywords
                                                mscgen--keywords-extra)))
         (x-types-regexp (regexp-opt mscgen--types) )
         (x-functions-regexp (regexp-opt mscgen--functions)))
    `((,x-keywords-regexp . font-lock-keyword-face)
      (,x-types-regexp . font-lock-type-face)
      (,x-functions-regexp . font-lock-function-name-face))))

(defconst mscgen-syntax-table
  (let ( (syn-table (make-syntax-table)))
    ;; python style comment: # &
    (modify-syntax-entry ?# "<" syn-table)
    (modify-syntax-entry ?\n ">" syn-table)
    syn-table)
  "Syntax table for mscgen comments.")


;;; Functions

(defun mscgen-completion-at-point ()
  "Complete the label or function at the current point."
  (interactive)
  (let* ((bds (bounds-of-thing-at-point 'symbol))
         (start (car bds))
         (end (cdr bds)))
    (list start end (append mscgen--keywords
                            mscgen--types
                            mscgen--functions) . nil)))

(defun mscgen-compile ()
  "Compile the current sequence diagram."
  (interactive)
  (let ((compile-command
         (read-from-minibuffer
          "Compile command: "
          (format
           "%s -T %s -i %s -o %s.%s"
           mscgen-executable
           mscgen-output-file-type
           (buffer-file-name)
           (file-name-sans-extension (buffer-file-name))
           mscgen-output-file-type))))
    (compile compile-command)))

(defun mscgen-insert-label-at-point ()
  "Quick insert a label at point."
  (interactive)
  (end-of-line)
  (insert (format "[label=\"%s\"];" (read-from-minibuffer "Label: "))))

(define-derived-mode mscgen-mode fundamental-mode "mscgen-mode"
  "Major mode for editing mscgen sequence diagrams. See
  http://www.mcternan.me.uk/mscgen/."

  (setq font-lock-defaults '((mscgen--font-lock-keywords)))
  (set-syntax-table mscgen-syntax-table)
  (setq-local comment-start "#")
  (setq-local comment-end "")
  (add-hook 'completion-at-point-functions
            'mscgen-completion-at-point nil 'local))

(provide 'mscgen-mode)


;;; mscgen-mode.el ends here

Finishing up

We need to reduce the GC threshold so we have small fast GCs for normal operation.

;; Make gc pauses faster by decreasing the threshold.
(setq gc-cons-threshold (* 2 1000 1000))

Inspiration

"Inspiration", in large part, means I blatantly stole from them. I started this list at ~3k lines of literate config, so I've missed folks.

© 2012 - 2023 · Home — Theme Simpleness