a space in between

My Literate Emacs Configuration

Table of Contents

1. theme

(load-theme 'modus-vivendi)
(set-face-background 'default "#111")
(set-face-background 'cursor "#c96")
(set-face-background 'isearch "#c60")
(set-face-foreground 'isearch "#eee")
(set-face-background 'lazy-highlight "#960")
(set-face-foreground 'lazy-highlight "#ccc")
(set-face-foreground 'font-lock-comment-face "#fc0")
(let ((line (face-attribute 'mode-line :underline)))
    (set-face-attribute 'mode-line          nil :overline   line)
    (set-face-attribute 'mode-line-inactive nil :overline   line)
    (set-face-attribute 'mode-line-inactive nil :underline  line)
    (set-face-attribute 'mode-line          nil :box        nil)
    (set-face-attribute 'mode-line-inactive nil :box        nil))

2. packages

I install most of my packages in a single block here at the beginning, although I'm starting to have some places where I install them inline with use-package, and do the configuration there. I'll probably continue to do a mix, based on whatever makes the cleanest looking config

(dolist (package '(add-node-modules-path
                   markdown-mode
                   mastodon
                   paredit
                   rainbow-delimiters
                   company-mode
                   eslintd-fix
                   embark-consult
                   hydra
                   inf-ruby
                   jq-mode
                   lispy
                   lsp-mode
                   lsp-ui
                   lsp-treemacs
                   languagetool
                   org-super-agenda
                   org-superstar
                   use-package
                   magit
                   meow
                   pocket-reader
                   rg
                   ripgrep
                   web-mode
                   web-beautify
                   wgrep
                   yasnippet-snippets))
  (straight-use-package package))
(setq straight-use-package-by-default "t")

3. Keybindings and Modal Editing

I'm a big fan of modal editing, which is vim-ish style editing where typically keystrokes don't actually add text, but instead run commands, until you put yourself in a special "editing" mode.

I use meow for more lightweight modal editing than vim, particulary since I don't have an affinity for vim's keybindings. With this I end up with 3 categories of keybindings I use for various purposes.

  1. First I have the keybindings from meow's normal mode, defined in the 3.1. Most of these are taken from the reccomended keybindings for qwerty layout provided by meow. These are generally for moving around the document. I've added a few custom commands, some of which are tied directly to commands which are applicable to every major mode, and some are tied to other keybindings, so I can control which command runs in different major modes via mode map keybindings.
  2. Second I make use of meow "keypad" mode to setup a series of "top-level" common commands. Key pad mode simulates having "C-c" pressed all the time. Using this I do things like
    • Emulate the Spacemacs double space keybinding for 'execute-extedned-command
    • Emulate some other common spacemacs keybindings like "SPC f s" for saving files
  3. Finally, I have a series of hydras which group related, commands into easy to use namespaces I generally access these hydras through keypad mode as well.

3.1. Modal Editing Keybindings

I use meow for light weight modal editing. Modal editing is vim style, where you can swap between modes for selecting text, running commands and editing text. I generally like the idea behind modal editing, because it makes it possible to run lots of commands with few key strokes, but I don't use full blown evil, because I have little attachment to the vim keystrokes. I was never a vim power user. Meow has no built in bindings, but offer some reccomended ones you can copy and paste, which I have done below with no other changes

(require 'meow)
(setq meow-keypad-leader-dispatch "C-c")
(defun meow-setup ()
  (setq meow-cheatsheet-layout meow-cheatsheet-layout-qwerty)
  ;; by default, in keypad mode, "g" serves as a ctrl-meta prefix
  ;; but I like to use SPC g s for magit, so I changed it to "v"
  (setq meow-keypad-ctrl-meta-prefix "v")
  (meow-motion-overwrite-define-key
   '("j" . meow-next)
   '("k" . meow-prev)
   '("<escape>" . ignore))
  (meow-leader-define-key
   ;; SPC j/k will run the original command in MOTION state.
   '("j" . "H-j")
   '("k" . "H-k")
   ;; Use SPC (0-9) for digit arguments.
   '("1" . meow-digit-argument)
   '("2" . meow-digit-argument)
   '("3" . meow-digit-argument)
   '("4" . meow-digit-argument)
   '("5" . meow-digit-argument)
   '("6" . meow-digit-argument)
   '("7" . meow-digit-argument)
   '("8" . meow-digit-argument)
   '("9" . meow-digit-argument)
   '("0" . meow-digit-argument)
   '("/" . meow-keypad-describe-key)
   '("?" . meow-cheatsheet))
  (meow-normal-define-key
   '("0" . meow-expand-0)
   '("9" . meow-expand-9)
   '("8" . meow-expand-8)
   '("7" . meow-expand-7)
   '("6" . meow-expand-6)
   '("5" . meow-expand-5)
   '("4" . meow-expand-4)
   '("3" . meow-expand-3)
   '("2" . meow-expand-2)
   '("1" . meow-expand-1)
   '("-" . negative-argument)
   '(";" . meow-reverse)
   '("," . meow-inner-of-thing)
   '("." . meow-bounds-of-thing)
   '("[" . meow-beginning-of-thing)
   '("]" . meow-end-of-thing)
   '("a" . meow-append)
   '("A" . meow-open-below)
   '("b" . meow-back-word)
   '("B" . meow-back-symbol)
   '("c" . meow-change)
   '("d" . meow-delete)
   '("D" . meow-backward-delete)
   '("e" . meow-next-word)
   '("E" . meow-next-symbol)
   '("f" . meow-find)
   '("g" . meow-cancel-selection)
   '("G" . meow-grab)
   '("h" . meow-left)
   '("H" . meow-left-expand)
   '("i" . meow-insert)
   '("I" . meow-open-above)
   '("j" . meow-next)
   '("J" . meow-next-expand)
   '("k" . meow-prev)
   '("K" . meow-prev-expand)
   '("l" . meow-right)
   '("L" . meow-right-expand)
   '("m" . meow-join)
   '("n" . meow-search)
   '("o" . meow-block)
   '("O" . meow-to-block)
   '("p" . meow-yank)
   '("q" . meow-quit)
   '("Q" . meow-goto-line)
   '("r" . meow-replace)
   '("R" . meow-swap-grab)
   '("s" . meow-kill)
   '("t" . meow-till)
   '("u" . meow-undo)
   '("U" . meow-undo-in-selection)
   '("v" . meow-visit)
   '("w" . meow-mark-word)
   '("W" . meow-mark-symbol)
   '("x" . meow-line)
   '("X" . meow-goto-line)
   '("y" . meow-save)
   '("Y" . meow-sync-grab)
   '("z" . meow-pop-selection)
   '("'" . repeat)
   '("<escape>" . ignore)
   ;; Custom keybindings
   '("/" . consult-line)
   ;; I use "C-c y" for cycling code folding in different major modes
   ;; by binding TAB to it, I can get single stroke code folding in any
   ;; mode, including org mode
   '("TAB" . "C-c y")))
  (meow-setup)
  (meow-global-mode 1)

3.2. Top Level Keybindings

(global-set-key (kbd "C-c k") 'my/server-shutdown)
(global-set-key (kbd "C-c <SPC>") 'execute-extended-command)
(global-set-key (kbd "C-c f s") 'save-buffer)
(global-set-key (kbd "C-c /") 'consult-ripgrep)
(global-set-key (kbd "C-c f t") 'treemacs)
(global-set-key (kbd "C-c g s") 'magit)
(global-set-key (kbd "C-c g y") 'yadm-status)
(global-set-key (kbd "C-c o") 'org-agenda-hydra/body)

3.3. Hydras

I have a series of hydras that are bound to a "C-c <prefix>" that cover a series of related fuctions. (ie all window management commands bound to "C-c w")

3.3.1. Windows Buffers and Projects

Managing windows and buffers. I bind this hydra to both "C-c w" and "C-c p" since it has stuff for both windows and projects. This allows me to easily switch contexts (ie, I come in it thinking I'm just going to switch between open buffers but then realize I need to open a brand new file)

(defhydra hydra-windows (:color blue)
  "Windows"
  ("TAB" meow-last-buffer "Swap last buffer" :column "Buffers")
  ("b" consult-buffer "Recent Buffers" :column "Buffers")
  ("f" projectile-find-file-dwim "Find file" :column "Buffers")
  ("<right>" windmove-right "Move right" :column "Movement")
  ("<left>" windmove-left "Move left" :column "Movement")
  ("<up>" windmove-up "Move up" :column "Movement")
  ("<down>" windmove-down "Move down" :column "Movement")
  ("k" delete-window "Delete window" :column "Windows")
  ("p" projectile-switch-project "Switch Project" :column "Projects")
  ("q" nil "quit"))
(global-set-key (kbd "C-c w") 'hydra-windows/body)
(global-set-key (kbd "C-c p") 'hydra-windows/body)

3.3.2. Code Navigation

Navigating within a code file

(defhydra hydra-nav (:color green)
  ("i" consult-imenu "Menu" :column "Navigation")
  ("m" consult-mark "Bookmark" :column "Navigation")
  ("l" consult-goto-line "Jump to line":column "Navigation")
  ("q" nil "quit"))
(global-set-key (kbd "C-c n") 'hydra-nav/body)

4. General Features

Here's where a set up a lot of features and capabilities that apply across all modes, things like completion, code folding, etc.

4.1. Completion/incremental narrowing

For completion I use vertico + consult + marginalia. Vertico offers a great interface for incremental narrowing in completion buffers, which means you can see the options change as you type. I use it along side prescient, which sorts and orders your options based on usage history so more frequent commands sort to the top

Consult provides a bunch of useful commands to search for things by completion. It provides an enhanced interface for ripgrep searches, buffer selection, search within file by line. In all consult commands, you are presented with a minibuffer with all the completion options and you can easily scroll between them with arrow keys, or continue to narrow with additional search terms. You can see any commands I have using consult in this config by searching for `consult-`

Marginalia adds helpful annotations about the items in a completion list. For example, with a list commands, it inclues the basic description of the command.

4.1.1. consult

(use-package consult
  :config
  ;; Ensures consult commands that are local to a project work with projectile
  (setq consult-project-root-function #'projectile-project-root) 
)

4.1.2. vertico

(straight-use-package 'vertico-prescient)
(use-package vertico :ensure t
  :init
  (vertico-mode)
  (vertico-prescient-mode 1)
  (prescient-persist-mode 1))

4.1.3. marginalia

(use-package marginalia
  :bind (:map minibuffer-local-map
         ("C-c C-a" . marginalia-cycle))
  :init
  (marginalia-mode))

4.2. Contextual menus

Embark is a great tool for quickly getting the commands that are available based on context. Think of it like the equivalent of a right click menu for when you just can't remember what the command is to do XYZ and want a useful prompt listing all the options for where your cursor is now

4.2.1. embark

(use-package embark
  :ensure t
  :bind (
    ("C-a" . embark-act)
    ("C-c a" . embark-act)))
(use-package embark-consult
  :after (embark consult))

4.3. Code Parsing

For code parsing, I use the built in treeset.el tree-sitter integration. This integration requires that you install treesitter grammars for any languages you want support for. These grammars are not actually emacs packages, they're just general github repos that compile down to a C binary use by tree-sitter. Ethan Leba outlined a clever approach we can take to install these grammars here, by usint straight.el. This function allows us to do that.

The actually installation is handled inside of the major mode configuration where the different grammars are used

4.3.1. Treesit grammar installation

(setq treesit-extra-load-path `(,(expand-file-name "ts-grammars" user-emacs-directory)))

(defun my/tree-sitter-compile-grammar (destination &optional src path)
  "Compile grammar at PATH, and place the resulting shared library in DESTINATION."
  (interactive "fWhere should we put the shared library? \nfWhat tree-sitter grammar are we compiling? \n")
  (make-directory destination 'parents)

  (let* ((default-directory
          (expand-file-name (or src "src/") (or path default-directory)))
         (parser-name
          (thread-last (expand-file-name "grammar.json" default-directory)
                       (json-read-file)
                       (alist-get 'name)))
         (emacs-module-url
          "https://raw.githubusercontent.com/casouri/tree-sitter-module/master/emacs-module.h")
         (tree-sitter-lang-in-url
          "https://raw.githubusercontent.com/casouri/tree-sitter-module/master/tree-sitter-lang.in")
         (needs-cpp-compiler nil))
    (message "Compiling grammar at %s" path)

    (url-copy-file emacs-module-url "emacs-module.h" :ok-if-already-exists)
    (url-copy-file tree-sitter-lang-in-url "tree-sitter-lang.in" :ok-if-already-exists)

    (with-temp-buffer
      (unless
          (zerop
           (apply #'call-process
                  (if (file-exists-p "scanner.cc") "c++" "cc") nil t nil
                  "parser.c" "-I." "--shared" "-o"
                  (expand-file-name
                   (format "libtree-sitter-%s%s" parser-name module-file-suffix)
                   destination)
                  (cond ((file-exists-p "scanner.c") '("scanner.c"))
                        ((file-exists-p "scanner.cc") '("scanner.cc")))))
        (user-error
         "Unable to compile grammar, please file a bug report\n%s"
         (buffer-string))))
    (message "Completed compilation")))

4.4. Errors and linting

I prefer flycheck over flymake because of it's flexibility. I also include flycheck indicator mode to get a nice indicator on the modeline with the count of errors and warnings

4.4.1. flycheck

(use-package flycheck :ensure t)
(use-package flycheck-indicator
  :ensure t
  :hook (flycheck-mode . flycheck-indicator-mode))

4.5. Code folding

For code folding I use a custom fork of https://github.com/emacs-tree-sitter/ts-fold. The original package is targeted at the elisp-tree-sitter package that added tree sitter support to Emacs <28. Since I'm using Emacs 29 with built in treesitter support, I forked the package and made changes to work with that. Eventually I may upstream those changes to the original package though

4.5.1. ts-fold

(use-package ts-fold
  :ensure t
  :straight (ts-fold :type git :host github :repo "AndrewSwerlick/ts-fold"))

4.6. Project Navigation

I use projectile for project management. I've never really tried project.el, but I'm happy with projectile so I don't feel the need to switch

4.6.1. projectile

(use-package projectile
  :ensure t
  :init
  (projectile-mode +1))

4.7. Git integration

I use magit for git integration, as many do. Additionally, I use yadm for management my dot files, and have added some special magit setup so I can use magit directly with yadm as well.

4.7.1. magit

  1. yadm integration
    (require 'tramp)
    (defun yadm-status ()
      "open magit for yadm"
      (interactive)
      (setenv  "SHELL"
               "/bin/bash")
      (magit-status "/yadm::"))
    
    (add-to-list 'tramp-methods
                 '("yadm"
                   (tramp-login-program "yadm")
                   (tramp-login-args (("enter")))
                   (tramp-login-env (("SHELL") ("/bin/sh")))
                   (tramp-remote-shell "/bin/sh")
                   (tramp-remote-shell-args ("-c"))))
    

4.8. Modeline

I like the clean, simple look of simple-mode line. I also use diminish to remove extra things from the mode line I don't want

(use-package simple-modeline
  :ensure t
  :hook (after-init . simple-modeline-mode))

(use-package diminish :ensure t)

4.9. File Browsing

Although I mostly get around files via projectile and ripgrep, I do like to have a directory structure sometimes. For that I use treemacs

4.9.1. treemacs

(use-package treemacs :ensure t)
(use-package treemacs-magit       :ensure t :after treemacs magit)
(use-package treemacs-projectile  :ensure t :after treemacs projectile)

4.10. Snippets

4.10.1. yasnippets

I use yasnippets for snippets. I want snippets to show up in company in every buffer where available, so I use a custom function to append it to all backends

(use-package yasnippet
  :ensure t
  :bind (:map yas-keymap ("M-." . yas-next-field-or-maybe-expand) ("M-," . yas-prev))
  :after (company)
  :config
  (yas-global-mode 1)
  (defun swerlick--company-backend-with-yas (backends)
      "Add :with company-yasnippet to company BACKENDS.
  Taken from https://github.com/syl20bnr/spacemacs/pull/179."
      (if (and (listp backends) (memq 'company-yasnippet backends))
      backends
      (append (if (consp backends)
              backends
          (list backends))
          '(:with company-yasnippet))))
  ;; add yasnippet to all backends
  (setq company-backends
      (mapcar #'swerlick--company-backend-with-yas company-backends)))

(use-package yasnippet-snippets :ensure t)

4.11. Custom Commands

Just some cusotm commands for various tasks. Some of them are bound to keybindings elsewhere, some are not

(defun my/copy-relative-buffer-path ()
  "Get the relative path of the current file"
  (interactive)
  (kill-new (file-relative-name buffer-file-name (projectile-project-root))))

(defun my/get-github-url ()
  "Get a github url that links directly to the current line of code.

Assumes the main branch exists and matches the current code file"
  (interactive)
  (let* ((origin (shell-command-to-string "git config --get remote.origin.url"))
        (repo (replace-regexp-in-string "\\(git@github.com:\\|.git\\)" "" origin))
        (file (file-relative-name buffer-file-name (projectile-project-root))))
        (linenum (line-number-at-pos))
        (kill-new (concat "https://github.com/" (substring repo 0 -1) "/blob/main/" file "#L" linenum))))

(defun my/server-shutdown ()
  "Save buffers, Quit, and Shutdown (kill) server"
  (interactive)
  (save-some-buffers)
  (kill-emacs))

4.12. Text Editing

4.12.1. Consider a period followed by a single space to be end of sentence

 (setq sentence-end-double-space nil)

4.12.2. Use spaces, not tabs, for indentation

 (setq-default indent-tabs-mode nil)

4.12.3. Display the distance between two tab stops as 4 characters wide

 (setq-default tab-width 4)

4.12.4. Indentation setting for various languages

 (setq c-basic-offset 4)
 (setq js-indent-level 2)
 (setq css-indent-offset 2)

4.12.5. Highlight matching pairs of parentheses

 (setq show-paren-delay 0)
 (show-paren-mode)

4.13. Terminal Integration

I use the kitty terminal and run emacs inside the terminal. I don't love the built in terminal tools in emacs, but kitty offers some great remote control features that let you control the terminal from other applications. I've written some custom functions that allow me to control my current terminal session from emacs, so I can basically create a new terminal window split to do terminal commands in the same terminal window as emacs

(cl-defun swerlick--kitty-command (command &optional (cwd default-directory) (focus-new-window t) (wait-to-exit t))
  (interactive
    (let ((string (read-string "Command: " nil 'my-history)))
      (list string)))
  (let* ((escaped-command (replace-regexp-in-string "\"" "\\\"" command nil t))
         (launch-opts (if focus-new-window "" "--keep-focus")) 
         (after-command (if wait-to-exit ";read '?[Press enter to exit]'" ""))
         (launch-command (format "kitty @ --to=$KITTY_LISTEN_ON launch %s --cwd=%s zsh -i -c \"%s%s\"" launch-opts cwd escaped-command after-command)))
    (message launch-command)
    (shell-command launch-command)))

(defun swerlick--open-terminal-window ()
  (interactive)
  (shell-command (format "kitty @ --to=$KITTY_LISTEN_ON launch --cwd=%s zsh -i" default-directory)))

4.14. misc

These are a bunch of general configs to vanilla emacs to make it behave more like I want

4.14.1. Write auto-saves and backups to separate directory

(make-directory "~/.tmp/emacs/auto-save/" t)
(setq auto-save-file-name-transforms '((".*" "~/.tmp/emacs/auto-save/" t)))
(setq backup-directory-alist '(("." . "~/.tmp/emacs/backup/")))

4.14.2. Do not move the current file while creating backup

(setq backup-by-copying t)

4.14.3. Disable lockfiles

(setq create-lockfiles nil)

4.14.4. Write customizations to a separate file instead of this file

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file t)

4.14.5. Enable mouse support

(xterm-mouse-mode 1)

4.14.6. recentf-mode

(recentf-mode 1)
(setq recentf-max-menu-items 25)
(setq recentf-max-saved-items 25)

4.14.7. Customize user interface.

 (menu-bar-mode 0)
 (when (display-graphic-p)
   (tool-bar-mode 0)
   (scroll-bar-mode 0))
 (setq inhibit-startup-screen t)
 (column-number-mode)

4.14.8. Map ESC to abort

(define-key key-translation-map (kbd "ESC") (kbd "C-g"))

4.14.9. Copy/paste integration

 (defun copy-from-osx ()
    (shell-command-to-string "pbpaste"))
  (defun paste-to-osx (text &optional push)
    (let ((process-connection-type nil))
      (let ((proc (start-process "pbcopy" "*Messages*" "pbcopy")))
        (process-send-string proc text)
        (process-send-eof proc))))
  (setq interprogram-cut-function 'paste-to-osx)
  (setq interprogram-paste-function 'copy-from-osx)

4.14.10. Easy minor mode configuration

(defvar auto-minor-mode-alist ()
  "Alist of filename patterns vs correpsonding minor mode functions, see `auto-mode-alist'
All elements of this alist are checked, meaning you can enable multiple minor modes for the same regexp.")

(defun enable-minor-mode-based-on-extension ()
  "Check file name against `auto-minor-mode-alist' to enable minor modes
the checking happens for all pairs in auto-minor-mode-alist"
  (when buffer-file-name
    (let ((name (file-name-sans-versions buffer-file-name))
          (remote-id (file-remote-p buffer-file-name))
          (case-fold-search auto-mode-case-fold)
          (alist auto-minor-mode-alist))
      ;; Remove remote file name identification.
      (when (and (stringp remote-id)
                 (string-match-p (regexp-quote remote-id) name))
        (setq name (substring name (match-end 0))))
      (while (and alist (caar alist) (cdar alist))
        (if (string-match-p (caar alist) name)
            (funcall (cdar alist) 1))
        (setq alist (cdr alist))))))

(add-hook 'find-file-hook #'enable-minor-mode-based-on-extension)

4.14.11. Disable bell

(setq ring-bell-function 'ignore)

4.14.12. Follow compilation mode buffers

(setq compilation-scroll-output t)

4.15. start-up

I use emacs from the command line, and I want to make sure even if I open a new emacs client in a different terminal it uses the same server session. This ensures that the first time I open emacs, it starts a server that future sessions can connect to.

(require 'server)
(unless (server-running-p)
  (server-start))

5. Ruby

Ruby is one of my primary programming languages, so I have alot of customizations to setup for it. Currently I'm using the new ruby-ts-mode with tree sitter integration +lsp-mode + flycheck. I also use `ts-fold` for code folding powered by tree sitter.

All my major modes get their own hydra, bound do "C-c m" for major mode related commands.

To setup the tree sitter grammar, I use this clever technique https://leba.dev/blog/2022/12/12/(ab)using-straightel-for-easy-tree-sitter-grammar-installations/ that uses straight packaging to download the grammar and compile it

(use-package tree-sitter-ruby
  :defer t
  :straight (:host github
           :repo "tree-sitter/tree-sitter-ruby"
           :post-build
           (my/tree-sitter-compile-grammar
            (expand-file-name "ts-grammars" user-emacs-directory))))

(use-package ruby-ts-mode
  :mode "\\(?:\\.rb\\|ru\\|rake\\|thor\\|jbuilder\\|gemspec\\|podspec\\|/\\(?:Gem\\|Rake\\|Cap\\|Thor\\|Vagrant\\|Guard\\|Pod\\)file\\)\\'"
  :interpreter "ruby"
  :bind (:map ruby-ts-mode-map ("C-c y" . ts-fold-toggle) ("C-c l" . hydra-ruby-mode/body))
  :after (flycheck)
  :config
  (defun swerlick--setup-ruby ()
    (setq flycheck-ruby-rubocop-executable "~/.bin/rubocop")
    (hs-minor-mode +1)
    (company-mode +1)
    (flycheck-mode +1)
    (eldoc-mode 1)
    (lsp)
    (require 'lsp-diagnostics)
    (lsp-diagnostics-flycheck-enable)
    (flycheck-add-next-checker 'lsp 'ruby-rubocop))
    (add-hook 'ruby-ts-mode-hook #'swerlick--setup-ruby))

5.1. Testing

Sets up rspec mode. Ensures it's easy to do pry debugging in a test by enabling inf ruby whenever we hit a breakpoint

(use-package rspec-mode
  :after (inf-ruby)
  :config  
  (defun setup-rspec-compilation ()
   (inf-ruby-enable-auto-breakpoint))
  (add-hook 'rspec-compilation-mode-hook 'setup-rspec-compilation))

5.2. Migrations

Convience methods to run rails migrations. This allows me to quick run and then rollback the migration I'm working on, and to run all migrations when I pull down code

(defcustom swerlick--rails-migration-compilation-dir nil
  "The directory emacs should use when running migration commands. Should be your rails root. If nil, we just use projectile root"
  :type 'string)

(defun swerlick--get-rails-migration-compilation-dir ()
  (or swerlick--rails-migration-compilation-dir (projectile-project-root)))

(defun swerlick--rails-run-migration-up ()
  (interactive)
  (let ((default-directory (swerlick--get-rails-migration-compilation-dir)))
    (when (string-match "db/migrate/\\([0-9]+\\)_.+\\.rb$" buffer-file-name)
      (compilation-start (format "VERSION=%s bundle exec rails db:migrate:up" (match-string 1 buffer-file-name))))))

(defun swerlick--rails-run-migration-down ()
  (interactive)
  (let ((default-directory (swerlikc--get-rails-migration-compilation-dir)))
    (when (string-match "db/migrate/\\([0-9]+\\)_.+\\.rb$" buffer-file-name)
      (compile (format "VERSION=%s bundle exec rails db:migrate:down" (match-string 1 buffer-file-name))))))

(defun swerlick--rails-run-migrations ()
  (interactive)
  (let ((default-directory (swerlick--get-rails-migration-compilation-dir)))
    (compile "bundle exec rails db:migrate")))

5.3. Editing

Just a quick method to autocorrect the current buffer via rubocop

  (defun swerlick--ruby-rubocop-fmt ()
    (interactive)
    (call-process-shell-command (format "bundle exec rubocop -x %s" buffer-file-name))
    (revert-buffer t t))

5.4. Hydra

The major mode hydra to make ruby specific methods easy to run

(defhydra hydra-ruby-mode (:color amaranth :title "")
    ("tt" rspec-verify "Run tests" :exit t :column "Testing")
    ("tl" rspec-verify "Run test at line" :exit t :column "Testing")
    ("t TAB" rspec-toggle-spec-and-target "Toggle spec/target" :column "Testing")
    ("mm" swerlick--rails-run-migrations "Run all migrations" :column "Migrations")
    ("md" swerlick--rails-run-migration-down "Rollback current migration" :column "Migrations")
    ("mu" swerlick--rails-run-migration-up "Run current migration" :column "Migrations")
    ("x" swerlick--ruby-rubocop-fmt "Rubocop fixup")
    ("q" nil "quit" :column ""))

6. Web

Here I have configrations for all of my web/frontend related programming. Generally I use js-mode for javascript and the typescript-mode package for typescript, with treesitter and lsp for both. For TSX, ERB and other html related items I used either web-mode directly or a modified version of webmode for tsx. With ERB and HTML, I don't use treesitter because it doesn't yet play nice with multi-language documents, which is what many of these web templating file types are.

6.1. Javascript

(use-package js
  :bind (:map js-mode-map ("C-c y" . ts-fold-toggle)
              ("C-c l" . hydra-js-mode/body))
  :after (flycheck)
  :config (defun swerlick--setup-javascript ()
            (setq flycheck-check-syntax-automatically '(save mode-enabled)
                  company-minimum-prefix-length 1)
            (flycheck-mode 1)
            (ts-fold-mode)
            (lsp)
            (eldoc-mode 1)
            (company-mode 1)
            (prettier-mode 1)
            (eslintd-fix-mode 1))
  (add-hook 'js-mode-hook #'swerlick--setup-javascript))

6.2. Typescript

(defun swerlick--setup-typescript ()
	        (setq flycheck-check-syntax-automatically '(save mode-enabled)
		          company-minimum-prefix-length 1)
	        (flycheck-mode 1)
	        (lsp)
	        (eldoc-mode 1)
	        (company-mode 1)
	        (prettier-mode 1)
	        (eslintd-fix-mode 1))

(use-package tree-sitter-typescript
  :defer t
  :straight (:host github
           :repo "tree-sitter/tree-sitter-typescript"
           :post-build
           (my/tree-sitter-compile-grammar
            (expand-file-name "ts-grammars" user-emacs-directory) "typescript/src/")))

(use-package tree-sitter-tsx
  :defer t
  :straight (:host github
           :repo "tree-sitter/tree-sitter-typescript"
           :post-build
           (my/tree-sitter-compile-grammar
            (expand-file-name "ts-grammars" user-emacs-directory) "tsx/src/")))

(use-package typescript-ts-mode
  :mode ("\.ts$")
  :bind (:map typescript-ts-mode-map
		      ("C-c l" . hydra-ts-mode/body))
  :after (flycheck)
  :config
  (add-hook 'typescript-ts-mode-hook #'swerlick--setup-typescript))

(use-package typescript-ts-mode
  :mode ("\.tsx$" . tsx-ts-mode)
  :bind (:map tsx-ts-mode-map
		      ("C-c l" . hydra-ts-mode/body))
  :after (flycheck)
  :config
  (add-hook 'tsx-ts-mode-hook #'swerlick--setup-typescript))

6.3. Javascript & Typescript testing

I have a number of custom functions to make working with tests easier, specifically toggling between the spec and target, and running yarn tests.

I don't run jest insides of emacs because I don't like how it behaves insides of the emulated terminal. Instead, I make use of the remote control features of my terminal, kitty. Basically I shell out to kitty in a new pane, and run jest there.

(defun swerlick--yarn-test ()
  (interactive)
  (let ((command (format "yarn test %s" (file-name-sans-extension buffer-file-name))))
    (swerlick--kitty-command command default-directory nil nil)))

(defun swerlick--jest-toggle-spec-and-target ()
  "Switch to the spec or the target file for the current buffer.
   If the current buffer is visiting a spec file, switches to the
   target, otherwise the spec."
  (interactive)
  (find-file (swerlick--jest-spec-or-target)))

(defun swerlick--jest-spec-file-p (a-file-name)
  "Return true if the specified A-FILE-NAME is a spec."
  (numberp (string-match ".test.tsx$" a-file-name)))

(defun swerlick--jest-buffer-is-spec-p ()
  "Return true if the current buffer is a spec."
  (and (buffer-file-name)
     (swerlick--jest-spec-file-p (buffer-file-name))))

(defun swerlick--jest-spec-file-for (a-file-name)
  "Find test for the specified file."
  (if (swerlick--jest-spec-file-p a-file-name)
      a-file-name
      (swerlick--jest-specize-file-name (expand-file-name  a-file-name))))

(defun swerlick--jest-target-file-for (a-spec-file-name)
  "Find the target for A-SPEC-FILE-NAME."
  (replace-regexp-in-string "\\.test\\.\\(tsx\\)$" ".\\1" a-spec-file-name))

(defun swerlick--jest-spec-or-target ()
  (if (swerlick--jest-buffer-is-spec-p)
    (swerlick--jest-target-file-for (buffer-file-name))
  (swerlick--jest-spec-file-for (buffer-file-name))))

(defun swerlick--jest-specize-file-name (a-file-name)
  "Return A-FILE-NAME but converted in to a spec file name."
    (replace-regexp-in-string "\\.\\(tsx\\)$" ".test.\\1" a-file-name))

6.4. Code formatting

(use-package prettier
  :ensure t
  :config
  (add-hook 'js-mode-hook 'prettier-mode)
  (add-hook 'web-mode-hook 'prettier-mode))

6.5. html

    (defun swerlick--setup-general-web ()
      (setq flycheck-check-syntax-automatically '(save mode-enabled))
      (eldoc-mode +1)
      (ts-fold-mode +1)
      (company-mode +1)
      (prettier-mode +1)
      (flycheck-mode))

  (add-to-list 'auto-mode-alist '("\\.webc\\'" . web-mode))
  (add-hook 'web-mode-hook #'swerlick--setup-general-web)

6.6. Hydras

(defhydra hydra-js-mode (:color blue :title "")
  ("tt" swerlick--yarn-test "Run tests" :column "Testing")
  ("t TAB" swerlick--jest-toggle-spec-and-target "Toggle test and target" :column "Testing")
  ("q" nil "quit"))

(defhydra hydra-ts-mode (:color blue :title "")
  ("tt" swerlick--yarn-test "Run tests" :column "Testing")
  ("t TAB" swerlick--jest-toggle-spec-and-target "Toggle test and target" :column "Testing")
  ("q" nil "quit"))

7. org

Of course org is a big part of my emacs setup. Generally, I use a sort of modified form of TDD where things got into buckets for things I need to be working on right now, vs things I can do later. I use slightly different systems and files for work vs personal tasks for a variety of reasons. I also have 2 "inboxes", one for tasks coming from my mobile phone, and one for links I've saved.

I sync up my org files across multiple devices using github. In the "sync" section below you can see some hooks I've configured that will automatically push and pull changes on save. I use Orgzly on my phone, and I've also set it up to sync using some tasker scripts.

I use org-super-agenda to get some nice grouping features in my agenda view, and generally have one main agenda view I call the organizer and bind to the custom command "o"

(defhydra org-agenda-hydra (:color yellow)
  ("a" org-agenda "Agenda")
  ("t" org-capture "Capture"))

(use-package org
  :ensure t
  :bind (:map org-mode-map ("C-c y" . org-cycle) ("C-c j" . org-goto-reveal))
  :config
  (defun org-goto-reveal ()
    "goto and then reveal a heading"
    (interactive)
    (org-goto)
    (org-show-subtree))
  (setq org-goto-interface 'outline-path-completion)
  (setq org-outline-path-complete-in-steps nil)
  (setq org-src-preserve-indentation t))

(use-package org-super-agenda
  :ensure t
  :config
  (my/org-agenda-config))

7.1. org-modern

Org-modern gives org a nice clean look with pretty bullets and highlighting

(use-package org-modern
  :ensure t
  :config
  (global-org-modern-mode))

7.2. agenda

This function sets up all my todo and agenda related pieces.

(defun my/org-agenda-config ()
  (org-super-agenda-mode t)
  (setq org-archive-location (concat org-directory "/action/archive.org::")
        org-capture-templates '(("t" "Todo" entry (file+headline "~/org/action/_work_organizer.org" "Inbox")
                                 "* TODO %?\n  %i\n  %a")
                                ("s" "scratch" entry (file+headline "~/org/knowledge/snippets.org" "To File")
                                 "* NOTE %?\n Entered on %U"))
        org-todo-keywords '((sequence "TODO" "WORKING" "BLOCKED" "LATER" "|" "DONE")
                            (sequence "ASK" "|" "ANSWERED")
                            (sequence "TO_REVIEW" "|" "ARCHIVE"))
        org-todo-keyword-faces '(("TODO" . org-warning)
                                 ("LATER" . "cyan")
                                 ("WORKING" . "magenta")
                                 ("BLOCKED" . "brightred")
                                 ("TO_REVIEW" . "brightcyan")
                                 ("ASK" . "yellow")
                                 ("ANSWERED" . (:foreground "blue" :weight bold)))
        org-agenda-sorting-strategy '(todo-state-up)
        org-agenda-files (directory-files-recursively "~/org" "org$")
        org-directory "~/org"
        org-agenda-window-setup "only-window"
        org-agenda-custom-commands '(("o" "Organizer" ((agenda "")
                                                       (todo "" ((org-super-agenda-groups '((:name "Now" :property ("agenda-group" "now"))
                                                                                            (:name "Inbox" :file-path "_mobile_inbox")
                                                                                            (:name "Ongoing" :property ("agenda-group" "ongoing"))
                                                                                            (:name "Soon" :property ("agenda-group" "soon"))
                                                                                            (:discard (:anything t))))))))))
  (add-hook 'org-agenda-mode-hook 'swerlick--fetch-org-from-git))
;; Patch org-mode to use vertical splitting
;; https://emacs.stackexchange.com/questions/33528/org-mode-agenda-is-taking-too-much-screen-estate
(defadvice org-prepare-agenda (after org-fix-split)
  (toggle-window-split))
(ad-activate 'org-prepare-agenda)

7.3. d20

I run dnd compaigns using org-d20, which provides me a few easy commands for managing combat in a campaign

(defhydra hydra-d20-mode (:color amaranth :title "")
  ("i" org-d20-initiative-dwim "Initiative")
  ("a" org-d20-initiative-add "Add")
  ("d" org-d20-damage "Damage")
  ("rr" org-d20-roll "Roll")
  ("rp" org-d20-roll-at-point "Roll at point")
  ("rl" org-d20-roll-last "Re-roll last")
  ("rt" org-d20-d20 "roll d20")
  ("q" nil "quit"))

(use-package org-d20
  :bind (:map org-mode ("C-c r" . hydra-d20-mode/body)))

7.4. inbox

(setq org-refile-targets
      '(("_personal_organizer.org" :maxlevel . 1)))

7.5. sync

These are the functions that power the sync on save features of my org setup

(defun swerlick--fetch-org-from-git ()
  (shell-command "git -C ~/org pull"))

(defun swerlick--org-status ()
  (replace-regexp-in-string "\n$" "" 
        (shell-command-to-string "git -C ~/org status --porcelain")))

(defun swerlick--org-status-dirty-p ()
  (not (string-match "^[[:blank:]]*$" (swerlick--org-status))))

(defun swerlick--sync-org-to-git ()
  (magit-status "~/org")
  (shell-command-to-string "git -C ~/org commit -a -m 'sync from personal'")
  (call-interactively 'magit-pull-from-upstream)
  (call-interactively 'magit-push-current-to-upstream))
                        
(defun swerlick--sync-org-to-git-if-dirty ()
  (if (swerlick--org-status-dirty-p)
    (swerlick--sync-org-to-git)))

(add-hook 'org-mode-hook
  (lambda ()
    (if (string-match "^_.*\\.org" (buffer-name))
      (add-hook 'after-save-hook 'swerlick--sync-org-to-git-if-dirty nil "t"))))

8. Niche Stuff

Here's where I've setup some random packages and things I'm experimenting with

8.1. pocket

(setq pocket-reader-open-url-default-function #'eww)

8.2. mastadon

(setq mastodon-instance-url "https://hachyderm.io"
      mastodon-active-user "thirdthoughts")

Author: Andrew Swerlick

Created: 2023-01-08 Sun 23:29

Validate