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
- https://github.com/alphapapa/magit-todos - see todos for project in magit
- https://github.com/raxod502/apheleia - nicer format-on-save behavior
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 thescreencapture
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'> © 2012 - 2023 · <a href=\"" org-static-blog-publish-url "\" >Home</a> — 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)))
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.
- Daviwil
- Nicolas Rougier, who I stole the beautiful table of contents from.