bitspook / spookmax.d

My Emacs configuration

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

  1. Clone this repo to ~/.emacs.d
  2. Open readme.org in emacs
  3. Press C-c C-v t to tangle all the blocks
  4. Restart Emacs. Elpaca will now clone all the packages, and then you’ll be ready for a spooky Emacs.

This setup make certain assumptions regarding its environment:

  1. Machine in use is Linux or MacOS
  2. Shell in use is fish

Spook max 👻

My Elpaca based emacs configuration.
  • Bootstrap
    ;; -*- lexical-binding: t -*-
    (setq package-enable-at-startup nil)
    
    ;;
    ;;; Bootstrap
    
    ;; Contrary to what many Emacs users have in their configs, you don't need
    ;; more than this to make UTF-8 the default coding system:
    (set-language-environment "UTF-8")
    
    ;; set-language-enviornment sets default-input-method, which is unwanted
    (setq default-input-method nil)
        

Initialize Elpaca itself

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

(defvar elpaca-installer-version 0.7)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
			:ref nil
			:files (:defaults (:exclude "extensions"))
			:build (:not elpaca--activate-package)))
(let* ((repo  (expand-file-name "elpaca/" elpaca-repos-directory))
 (build (expand-file-name "elpaca/" elpaca-builds-directory))
 (order (cdr elpaca-order))
 (default-directory repo))
  (add-to-list 'load-path (if (file-exists-p build) build repo))
  (unless (file-exists-p repo)
    (make-directory repo t)
    (when (< emacs-major-version 28) (require 'subr-x))
    (condition-case-unless-debug err
  (if-let ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
	   ((zerop (call-process "git" nil buffer t "clone"
				 (plist-get order :repo) repo)))
	   ((zerop (call-process "git" nil buffer t "checkout"
				 (or (plist-get order :ref) "--"))))
	   (emacs (concat invocation-directory invocation-name))
	   ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
				 "--eval" "(byte-recompile-directory \".\" 0 'force)")))
	   ((require 'elpaca))
	   ((elpaca-generate-autoloads "elpaca" repo)))
      (progn (message "%s" (buffer-string)) (kill-buffer buffer))
    (error "%s" (with-current-buffer buffer (buffer-string))))
((error) (warn "%s" err) (delete-directory repo 'recursive))))
  (unless (require 'elpaca-autoloads nil t)
    (require 'elpaca)
    (elpaca-generate-autoloads "elpaca" repo)
    (load "./elpaca-autoloads")))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
  • Install use-package
;; Install use-package support
(elpaca elpaca-use-package
  ;; Enable :elpaca use-package keyword.
  (elpaca-use-package-mode)
  ;; Assume :elpaca t unless otherwise specified.
  (setq elpaca-use-package-by-default t))

(elpaca-wait)

Helper utilities

  • Ergonomically create keymaps
    (defmacro spook--defkeymap (name prefix &rest bindings)
      "Create a new NAME-keymap bound to PREFIX, with BINDINGS.
    
    Usage:
      (spook--defkeymap \"spook-git\" \"C-c g\"
        '(\"s\" . magit-status))
    "
      (let* ((keymap-name (intern (concat name "-keymap")))
             (keymap-alias (intern name))
             (keymap-bindings (mapcar
                               (lambda (binding)
                                 (let ((binding (eval binding)))
                                   `(define-key ,keymap-name (kbd ,(car binding)) #',(cdr binding))))
                               bindings)))
        `(progn
           (defvar ,keymap-name (make-sparse-keymap))
           (defalias ',keymap-alias ,keymap-name)
           (global-set-key (kbd ,prefix) ',keymap-alias)
           ,@keymap-bindings)))
        
  • Utilities for helping with scratch buffer

    This exactly function was introduced at or before Emacs version 29.1.

    (defun spook--get-or-create-scratch ()
      "Switch to *scratch* buffer. Create if it doesn't already exist"
      (interactive)
      (let ((s-buf (get-buffer "*scratch*")))
        (switch-to-buffer (get-buffer-create "*scratch*"))
        (when (not s-buf) (emacs-lisp-mode))))
        

    For firefox, I want to capture whatever I am reading and open the captured content in baby-window. But there is a fuck-up here that I’ve been unable to fix. On aborting the capture, the window layout gets messed up.

    (defun spook--baby-window (&optional baby-size)
      "Create a baby of active window of BABY-SIZE height.
    A baby-window is a small window below active-window, like
    DevConsole in a browser. Depending on the active-window,
    baby-window contains a different application. If the prefix-arg
    is nil, baby-window always open *scratch* buffer."
      (interactive)
      (let ((baby-window (split-window-below (or baby-size -20))))
        (select-window baby-window)
        (spook--get-or-create-scratch)))
        
  • Profiles

    Let’s introduce a concept of profiles to change the configuration based on different scenarios. Right now I run my Emacs on two machines, but instead for adding checks for which machine I am on right now, we’ll create a default configuration, and modify it based on which profiles are active right now. At startup, we’ll perform the checks to automatically enable certain profiles.

    A profile is a cons cell of (name . metadata)

    (defvar spook--active-profiles '()
      "Change things slightly based on different profiles.")
        
    • On mac without external monitor
      (when (eq system-type 'darwin)
        (push '(small-screen . t) spook--active-profiles))
              

Preliminary setup

  • Start emacs as a server
    (server-start)
        
  • Unset annoying keybindings
    (global-unset-key (kbd "C-x C-z"))
    (global-unset-key (kbd "C-z"))
    (global-unset-key (kbd "C-h h"))
        
  • Set a custom-file so Emacs won’t put customized entries in my init.el which gets overwritten every time I tangle spookmax.d
    (setq custom-file (concat user-emacs-directory "custom.el"))
        
  • Disable the ugly-ass toolbar, scroll-bars and menu-bar
    (setq inhibit-startup-screen t
          use-dialog-box nil)
    (tool-bar-mode -1)
    (scroll-bar-mode -1)
    (menu-bar-mode -1)
    (tooltip-mode -1)
        
  • Make emacs a little transparent
    (set-frame-parameter (selected-frame) 'alpha '(98 . 95))
    (add-to-list 'default-frame-alist '(alpha . (98 . 95)))
        
  • Disable native-comp warnings
    (setq native-comp-async-report-warnings-errors 'silent)
        
  • UI fixes copied from Doom https://github.com/hlissner/doom-emacs/blob/develop/core/core-ui.el
    • Scrolling
      ;;; Scrolling
      
      (setq hscroll-margin 2
            hscroll-step 1
            ;; Emacs spends too much effort recentering the screen if you scroll the
            ;; cursor more than N lines past window edges (where N is the settings of
            ;; `scroll-conservatively'). This is especially slow in larger files
            ;; during large-scale scrolling commands. If kept over 100, the window is
            ;; never automatically recentered.
            scroll-conservatively 101
            scroll-margin 0
            scroll-preserve-screen-position t
            ;; Reduce cursor lag by a tiny bit by not auto-adjusting `window-vscroll'
            ;; for tall lines.
            auto-window-vscroll nil
            ;; mouse
            mouse-wheel-scroll-amount '(2 ((shift) . hscroll))
            mouse-wheel-scroll-amount-horizontal 2)
              
    • Cursors
      ;;; Cursor
      (blink-cursor-mode -1)
      
      ;; Don't blink the paren matching the one at point, it's too distracting.
      (setq blink-matching-paren nil)
      
      ;; Don't stretch the cursor to fit wide characters, it is disorienting,
      ;; especially for tabs.
      (setq x-stretch-cursor nil)
              
    • Window/Frame
      ;; A simple frame title
      (setq frame-title-format '("%b")
            icon-title-format frame-title-format)
      
      ;; Don't resize the frames in steps; it looks weird, especially in tiling window
      ;; managers, where it can leave unseemly gaps.
      (setq frame-resize-pixelwise t)
      
      ;; But do not resize windows pixelwise, this can cause crashes in some cases
      ;; when resizing too many windows at once or rapidly.
      (setq window-resize-pixelwise nil)
      
      ;; Favor vertical splits over horizontal ones. Monitors are trending toward
      ;; wide, rather than tall.
      (setq split-width-threshold 160
            split-height-threshold nil)
              
    • Minibuffer
      ;;
      ;;; Minibuffer
      
      ;; Allow for minibuffer-ception. Sometimes we need another minibuffer command
      ;; while we're in the minibuffer.
      (setq enable-recursive-minibuffers t)
      
      ;; Show current key-sequence in minibuffer ala 'set showcmd' in vim. Any
      ;; feedback after typing is better UX than no feedback at all.
      (setq echo-keystrokes 0.02)
      
      ;; Expand the minibuffer to fit multi-line text displayed in the echo-area. This
      ;; doesn't look too great with direnv, however...
      (setq resize-mini-windows 'grow-only)
      
      ;; Typing yes/no is obnoxious when y/n will do
      (setf use-short-answers t)
      
      ;; Try to keep the cursor out of the read-only portions of the minibuffer.
      (setq minibuffer-prompt-properties '(read-only t intangible t cursor-intangible t face minibuffer-prompt))
      (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
      
      ;; Don't resize the frames in steps; it looks weird, especially in tiling window
      ;; managers, where it can leave unseemly gaps.
      (setq frame-resize-pixelwise t)
      
      ;; But do not resize windows pixelwise, this can cause crashes in some cases
      ;; when resizing too many windows at once or rapidly.
      (setq window-resize-pixelwise nil)
              
  • Allow selection to be deleted, generally expected behavior during editing. I tried to not have this on by default, but I am finding that to be increasingly annoying.
    (delete-selection-mode +1)
        
  • Indentation and whitespace
    (setq spook--indent-width 2)
    (setq-default tab-width spook--indent-width)
    (setq-default indent-tabs-mode nil)
        

    From: https://github.com/susam/emfy/blob/main/.emacs#L26

    (setq-default indicate-empty-lines t)
    (setq-default indicate-buffer-boundaries 'left)
    
    ;; Consider a period followed by a single space to be end of sentence.
    (setq sentence-end-double-space nil)
    
    (setq create-lockfiles nil)
        

    I got sick of manually calling whitespace cleanup all the trim. Cleanup whitespace.

    (use-package whitespace-cleanup-mode
      :config
      (global-whitespace-cleanup-mode +1))
        
  • Fill column for auto-formatting/filling paragraphs.
    (setq-default fill-column 100)
        
  • Introspection

    Setup which-key for easy keys discovery

    (use-package which-key
      :config
      (which-key-mode t))
        
  • Highlighting
    (global-hl-line-mode +1)
    
    (use-package highlight-symbol
      :hook (prog-mode . highlight-symbol-mode)
      :config
      (setq highlight-symbol-idle-delay 0.3))
        
  • Line numbers
    (global-display-line-numbers-mode 1)
        
  • Window management
    • Custom window keybindings
      (spook--defkeymap "spook-windows" "C-c s-w"
        '("-" . split-window-below)
        '("_" . spook--baby-window)
        '("/" . split-window-right)
        '("d" . delete-window)
        '("m" . delete-other-windows)
        '("o" . other-window)
        '("h" . windmove-left)
        '("j" . windmove-down)
        '("k" . windmove-up)
        '("l" . windmove-right)
        '("w" . ace-window))
              
    • Install ace-window for some nice utilities.
      (defun spook--aw-kill-buffer-in-window (win)
        "Kill the buffer shown in window WIN."
        (kill-buffer (window-buffer win)))
      
      (defun spook--aw-kill-buffer-and-window (win)
        "Kill the buffer shown in window WIN and window itself."
        (kill-buffer (window-buffer win))
        (delete-window win))
      
      (use-package ace-window
        :config
        (setq aw-dispatch-always t)
        (global-set-key (kbd "C-c w") 'ace-window)
        (setq aw-dispatch-alist
              '((?d spook--aw-kill-buffer-in-window "Kill buffer in window")
                (?s aw-swap-window "Swap Windows")
                (?S aw-move-window "Move Window")
                (?c aw-copy-window "Copy Window")
                (?w aw-flip-window)
                (?b aw-switch-buffer-in-window "Select Buffer")
                (?B aw-switch-buffer-other-window "Switch Buffer Other Window")
                (?k aw-delete-window "Delete Window")
                (?K spook--aw-kill-buffer-and-window "Kill buffer in window")
                (?= aw-split-window-fair "Split Fair Window")
                (?- aw-split-window-vert "Split Vert Window")
                (?/ aw-split-window-horz "Split Horz Window")
                (?m delete-other-windows "Delete Other Windows")
                (?? aw-show-dispatch-help))
              aw-keys '(?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9)))
              
  • Workspace management with perspective

    I was using eyebrowse earlier, but I don’t like its reliance on desktop-mode to save state. Let’s give perspective a shot

    (use-package perspective
      :init
      (setq persp-mode-prefix-key (kbd "C-c C-w"))
      :config
      (persp-mode +1))
        
  • Buffer management
    (spook--defkeymap
     "spook-buffers" "C-c b"
     '("b" . switch-to-buffer)
     '("n" . next-buffer)
     '("p" . previous-buffer)
     '("n" . next-buffer)
     '("d" . kill-current-buffer)
     '("s" . spook--get-or-create-scratch))
        
  • Font size
    (defvar spook--font-size 11)
    (when (assoc 'small-screen spook--active-profiles)
      (setq spook--font-size 14))
    (set-face-attribute 'default nil :height (* 10 spook--font-size))
        
  • [Ma]git

    Magit uses project-switch-commands which are present only in more recent project.el project.

    (use-package project)
        
    (use-package transient
      :ensure (transient :host github :repo "magit/transient" :branch "main"))
    
    (use-package magit 
      :ensure (magit :host github :repo "magit/magit" :branch "main")
      :config
      (setq magit-display-buffer-function 'magit-display-buffer-fullframe-status-v1
            magit-bury-buffer-function #'magit-restore-window-configuration))
        
    • git-link so I can copy link to lines in files because evidently, I am doing that a lot
      (use-package git-link
        :config
        (setf git-link-use-commit t))
              
    • Buncha nice keybindings.
      (spook--defkeymap "spook-git" "C-c g"
        '("s" . magit-status)
        '("b" . magit-blame)
        '("g" . magit-dispatch))
              
    • Magit Forge
      ;; (use-package forge
      ;;   :after magit)
              
  • Keep backup/auto-save files out of my vc
    (setq
     backup-dir "~/.emacs.d/bakups"
     backup-directory-alist `((".*" . ,backup-dir))
     auto-save-file-name-transforms `((".*" ,backup-dir t))
     create-lockfiles nil)
        
  • Setup PATH from shell
    (use-package exec-path-from-shell
      :config
      (exec-path-from-shell-initialize))
        

Org mode

  • Install latest org-mode. Elpaca will install the latest org-mode, instead of older version pre-packaged with emacs
       (use-package org
         :config
         (eval-after-load 'org-mode
    	(org-link-set-parameters
    	 "yt"
    	 :follow #'spook-org--follow-yt-link
    	 :export #'spook-org--export-yt-link))
         (add-hook
          'org-mode-hook
          (lambda () (display-line-numbers-mode -1))))
        
  • Other settings
    (setq
     org-startup-indented t
     org-startup-folded t
     org-agenda-window-setup "only-window"
     org-directory "~/Documents/org"
     org-agenda-diary-file (concat org-directory "/diary.org.gpg")
     org-inbox-file (concat org-directory "/TODOs.org")
     org-agenda-files (list org-inbox-file (expand-file-name "work/on.org" org-directory))
     ;;Todo keywords I need
     org-todo-keywords '((sequence "TODO(t)" "DOING(n)" "|" "DONE(d)" "CANCELED(c@)"))
     org-todo-keyword-faces '(("DOING" . "DeepSkyBlue")
                              ("CANCELED" . org-done))
     org-default-notes-file (concat org-directory "/refile.org")
     org-refile-targets '((org-agenda-files . (:maxlevel . 6)))
     org-capture-templates
     '(("i" "Idea" entry (file+headline org-inbox-file "Inbox") "* %?\t\t:idea:\n%U")
       ("t" "Todo" entry (file+headline org-inbox-file "Inbox") "* TODO %?\n%U\n[[%F]]"))
     org-log-into-drawer "LOGBOOK"
     org-log-done "time"
     org-clock-report-include-clocking-task t
     org-clock-into-drawer t
     org-fontify-done-headline t
     org-enforce-todo-dependencies t
     org-agenda-overriding-columns-format "%80ITEM(Task) %6Effort(XP){+}"
     org-columns-default-format org-agenda-overriding-columns-format
     org-use-property-inheritance t
     org-confirm-babel-evaluate nil
     org-id-link-to-org-use-id t
     org-fold-catch-invisible-edits 'show
     org-cycle-separator-lines 0
     org-export-allow-bind-keywords t)
    
    ;; org-mode settings
    (with-eval-after-load 'org
      (org-indent-mode t)
      (require 'org-id))
        
  • Keybindings
    (global-set-key (kbd "C-c c") #'org-capture)
    
    (spook--defkeymap
     "spook-org" "C-c o"
     '("a" . org-agenda-list)
     '("A" . org-agenda)
     '("c" . org-capture)
     '("C" . org-clock-goto)
     '("o" . consult-org-agenda))
        
  • org-super-agenda

    More/better structure in agenda view.

    (use-package org-super-agenda
      :config
      (org-super-agenda-mode t)
      (setq org-super-agenda-groups
            '((:name "Work" :tag "work" :order 1)
              (:name "In Progress" :todo "DOING" :order 1)
              (:name "Projects" :tag "project" :order 3)
              (:name "Home" :tag "home" :order 2)
              (:name "Study" :tag "study" :order 4)
              (:name "Inbox" :tag "inbox" :order 4)
              (:name "Habits" :tag "habit" :order 5))))
        
  • org-babel
    (use-package ob-http)
    
    (with-eval-after-load 'org
      (org-babel-do-load-languages
       'org-babel-load-languages
       '((emacs-lisp . t)
         (plantuml . t)
         (shell . t)
         (sql . t)
         (sqlite . t)
         (lisp . t)
         (js . t)
         (http . t))))
        
  • Allow adding HTML class/id to exported src blocks

    Org mode don’t allow adding custom HTML class or id to exported src blocks, but I’ve found myself in need of this functionality when customizing published projects.

    (defun spook--org-src-block-html-attrs-advice (oldfun src-block contents info)
      "Add class, id or data-* CSS attributes to html source block output.
    
    Allows class, id or data  attributes to be added to a source block using
    #attr_html:
    
        #+ATTR_HTML: :class myclass :id myid
        #+begin_src python
        print(\"Hi\")
        #+end_src
    "
      (let* ((old-ret (funcall oldfun src-block contents info))
             (class-tag (org-export-read-attribute :attr_html src-block :class))
             (data-attr (let ((attr (org-export-read-attribute :attr_html src-block :data)))
                          (when attr (split-string attr "="))))
             (id-tag (org-export-read-attribute :attr_html src-block :id)))
        (if (or class-tag id-tag  data-attr)
            (concat
             "<div "
             (if class-tag (format "class=\"%s\" " class-tag))
             (if id-tag (format "id=\"%s\" " id-tag))
             (if data-attr (format "data-%s=\"%s\" " (car data-attr) (cadr data-attr)))
             ">"
             old-ret
             "</div>")
          old-ret)))
    
    (advice-add 'org-html-src-block :around #'spook--org-src-block-html-attrs-advice)
        
  • Support exporting code blocks with syntax-highlighting
    (use-package htmlize)
        
  • Custom links
    • yt:// links
      • Open yt:// links in mpv if mpv is present
      • Open yt:// links in browser if mpv isn’t installed or prefix-argument is provided with org-open-at-point (i.e C-c C-o)
           (defun spook-org--follow-yt-link (path prefix)
      	(let* ((url (format "https:%s" path))
      	       (proc-name (format "*yt://%s*" url)))
      	  (if (and prefix (executable-find "mpv"))
      	      (browse-url url)
      	    (make-process :name proc-name :buffer proc-name :command `("mpv" ,url))
      	    (message "Launched mpv in buffer: %s" proc-name))))
      
           (defun spook-org--export-yt-link (path desc backend)
      	(when (eq backend 'html)
      	  (let* ((video-id (cadar (url-parse-query-string path)))
      		 (url (if (string-empty-p video-id) path
      			(format "//youtube.com/embed/%s" video-id))))
      	    (format
      	     "<iframe width=\"560\" height=\"315\" src=\"%s\" title=\"%s\" frameborder=\"0\" allowfullscreen></iframe>"
      	     url desc))))
              

Modal editing with Meow

Let’s get some modal editing with some spice. I have used Evil mode with Spacemacs, I was going to configure Evil, but let’s give meow a shot!

  • Meow qwerty setup copied from https://github.com/meow-edit/meow/blob/master/KEYBINDING_QWERTY.org
    (defun meow-setup ()
      (setq meow-cheatsheet-layout meow-cheatsheet-layout-qwerty)
      (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)))
        
(use-package meow
  :config
  (setf meow-use-clipboard t)
  (meow-global-mode)
  (meow-setup))
(elpaca-wait)
  • Normal mode-keybindings. Mostly mimicking vim
    (meow-normal-define-key
     '("z" . spook-fold)
     '("/" . "C-s")
     '("?" . "C-r"))
        
  • Leader keybindings
    (meow-leader-define-key
     '("/" . consult-git-grep)
     '("p" . projectile-command-map)
     '("e" . flycheck-command-map)
     '("w" . ace-window)
     '("b" . spook-buffers)
     '("G" . spook-git)
     '("o" . spook-org)
     '("n" . spook-notes))
        
  • Keychords
    (use-package key-chord
      :config
      (setf key-chord-two-keys-delay 0.1)
      (key-chord-mode 1)
      (key-chord-define meow-insert-state-keymap "fd" #'meow-insert-exit))
        

Completion UI

  • Orderlies adds matches completion candidates by space-separated patterns in any order
    (use-package orderless
      :config
      (setq completion-styles '(orderless)))
        
  • Vertico for completion UI
     (use-package vertico
       :ensure (:files (:defaults "extensions/*.el"))
       :init (vertico-mode +1)
       :config
       (define-key vertico-map (kbd "C-c ?") #'minibuffer-completion-help))
    
     (use-package vertico-directory
       :after vertico
       :ensure nil
       ;; More convenient directory navigation commands
       :bind (:map vertico-map
    		  ("C-h" . vertico-directory-delete-word))
       ;; Tidy shadowed file names
       :hook (rfn-eshadow-update-overlay . vertico-directory-tidy))
    
     (use-package vertico-quick
       :after vertico
       :ensure nil
       :bind (:map vertico-map
    		  ("C-q" . vertico-quick-insert))
       ;; Tidy shadowed file names
       :hook (rfn-eshadow-update-overlay . vertico-directory-tidy))
    
     ;; Persist history over Emacs restarts. Vertico sorts by history position.
     (use-package savehist
       :ensure nil
       :init
       (savehist-mode +1))
    
     ;; Emacs 28: Hide commands in M-x which do not work in the current mode.
     ;; Vertico commands are hidden in normal buffers.
     (setq read-extended-command-predicate
    	  #'command-completion-default-include-p)
        
  • Marginalia adds pretty information to completions. It’s pretty, useful, and recommended by embark (it provides extra information to embark)
    ;; Enable richer annotations using the Marginalia package
    (use-package marginalia
      :bind (:map minibuffer-local-map
             ("M-A" . marginalia-cycle))
      :init (marginalia-mode +1))
        
  • Consult for enhanced commands
    (use-package consult
      :init
      (setq consult-project-root-function #'projectile-project-root)
      :config
      (consult-customize consult-theme :preview-key '(:debounce 0.5 any))
    
      (global-set-key (kbd "C-s") #'consult-line)
      (global-set-key (kbd "C-r") #'consult-line-multi)
      (global-set-key (kbd "C-x b") #'consult-buffer)
      (define-key spook-buffers-keymap (kbd "b") #'consult-buffer)
      (define-key spook-buffers-keymap (kbd "B") #'consult-buffer-other-window)
    
      ;; better yank which show kill-ring for selection
      (global-set-key (kbd "C-y") #'consult-yank-pop)
      (meow-leader-define-key
       '("/" . consult-ripgrep))
      (meow-normal-define-key
       '("p" . consult-yank-pop)
       '("Q" . consult-goto-line)
       '("X" . consult-focus-lines)))
    
    (setq xref-show-xrefs-function #'consult-xref
          xref-show-definitions-function #'consult-xref)
    
    (recentf-mode +1)
    
    (use-package consult-flycheck
      :config
      (define-key flycheck-command-map (kbd "l") #'consult-flycheck))
    
    (use-package embark-consult
      :after (embark consult)
      :demand t
      :hook
      (embark-collect-mode . consult-preview-at-point-mode))
        

Contextual actions

  • embark allow contextual actions, like opening buffers in other window from minibuffer and a lot more
    (defun spook--embark-act-no-quit ()
      "(embark-act), but don't quit the minibuffer"
      (interactive)
      (let ((embark-quit-after-action nil))
        (embark-act)))
    
    (use-package embark
      :bind
      (("C-," . embark-act)
       ("C-." . embark-dwim)
       ("C-h b" . embark-bindings)
       ("C-<" . spook--embark-act-no-quit)))
        

More powerful editing

  • wgrep for editing grep buffers
    (use-package wgrep)
        
  • undo-tree-mode for more powerful undo
    (use-package undo-tree
      :config
      (global-undo-tree-mode t) 
      (global-set-key (kbd "C-/") #'undo)
      (global-set-key (kbd "C-S-/") #'undo-tree-redo)
      (setq undo-tree-history-directory-alist `(("." . ,(expand-file-name ".cache" user-emacs-directory)))))
        
  • embrace for wrapping pair manipulation
    (use-package embrace
      :config
      (add-hook 'org-mode-hook 'embrace-org-mode-hook)
      (meow-normal-define-key
       '("S" . embrace-commander)))
        
  • yasnippet for templates
    (use-package yasnippet
      :config
      (add-hook 'prog-mode-hook #'yas-minor-mode))
    
    (use-package yasnippet-snippets
      :after yasnippet)
        

Programming

  • Show trailing whitespace in programming files
    (add-hook 'prog-mode-hook #'(lambda () (setq-local show-trailing-whitespace t)))
        
  • Wrapping text in parens, quotes etc
    (show-paren-mode 1)
    (electric-pair-mode 1)
        
  • Code folding
    (use-package ts-fold
      :ensure (ts-fold :type git :host github :repo "emacs-tree-sitter/ts-fold")
      :config
      (global-ts-fold-mode)
      (global-ts-fold-indicators-mode)
      (spook--defkeymap
       "spook-fold" "C-c f"
       '("O" . ts-fold-open-all)
       '("o" . ts-fold-open-recursively)
       '("C" . ts-fold-close-all)
       '("c" . ts-fold-close)
       '("z" . ts-fold-toggle)))
        
  • Flycheck for getting those in-buffer warnings errors.
    (use-package flycheck
      :init
      (global-flycheck-mode t)
      ;; alias is needed for using the keymap in meow
      (defalias 'flycheck-command-map flycheck-command-map))
        
  • Projectile for managing projects.
    (use-package projectile
      :init (projectile-mode +1)
      :bind (:map projectile-mode-map
                  ("s-p" . projectile-command-map)
                  ("C-c p" . projectile-command-map)))
        
  • Company mode

    I think I have a general idea of what it does, but still fuzzy on details. This stuff is usually taken for granted; I’ve been taking it for granted with Spacemacs for a while now I suppose.

    (use-package company
      :init (global-company-mode +1))
        

    company-box-mode adds icons and colors to company options.

    (use-package company-box
      :hook (company-mode . company-box-mode))
        
  • Reformatter allow creating buffer/region formatters from any command.
    (use-package reformatter
      :config
      (reformatter-define prettier-format
        :program (expand-file-name "node_modules/.bin/prettier"
                                   (locate-dominating-file (buffer-file-name) "node_modules/.bin/prettier"))
        :args `("--stdin-filepath" ,(buffer-file-name)))
      :hook (web-mode . prettier-format-on-save-mode))
        
  • Direnv is pretty essential for my dev workflow.
    (use-package direnv
      :config
      (direnv-mode)
      (when (not (boundp 'warning-suppress-types))
        (setq warning-suppress-types nil))
      (add-to-list 'warning-suppress-types '(direnv)))
        
  • Eglot to provide LSP support.
    ;; Need to be manuall installed so we get latest version
    ;; (use-package jsonrpc
    ;;   :ensure '(jsonrpc :repo "https://git.savannah.gnu.org/git/emacs.git"
    ;;                     :files ("lisp/jsonrpc.el")))
    ;; (use-package eldoc
    ;;   :ensure '(eldoc :repo "https://git.savannah.gnu.org/git/emacs.git"
    ;;                   :files ("lisp/emacs-lisp/eldoc.el")))
    
    ;; (use-package eglot)
    
    ;; Looks like jsonrpc logging make eglot super laggy for typescript.
    ;; https://old.reddit.com/r/emacs/comments/1447fy2/looking_for_help_in_improving_typescript_eglot/
    ;; https://www.reddit.com/r/emacs/comments/16vixg6/how_to_make_lsp_and_eglot_way_faster_like_neovim/
    (fset #'jsonrpc--log-event #'ignore)
    (setq eglot-events-buffer-size 0
          eglot-sync-connect nil
          eglot-connect-timeout nil
          company-idle-delay 0
          company-minimum-prefix-length 1)
    (add-hook 'focus-out-hook 'garbage-collect)
        

Lisp

Lispy for some nasty lisp structural editing.

(use-package lispy
  :hook ((emacs-lisp-mode . lispy-mode)
         (lisp-mode . lispy-mode))
  :config
  (setf lispy-colon-p nil))

Elsa provides very nice static-analysis and more for elisp programming. First time I am trying this, hopefully it does what it says on the box without much fuss.

(use-package flycheck-elsa
  :after elsa
  :hook (emacs-lisp-mode . flycheck-elsa-setup))
  • Common Lisp

    Sly for interactive development.

    (use-package sly
      :hook ((lisp-mode . sly-mode))
      :config
      (setq org-babel-lisp-eval-fn #'sly-eval
            inferior-lisp-program "sbcl")
      (add-hook
       'sly-mrepl-hook
       (lambda () (set-face-foreground 'sly-mrepl-output-face "khaki3"))))
        

    sly-asdf add asdf integration to sly.

    (use-package sly-asdf
      :config
      (add-to-list 'sly-contribs 'sly-asdf 'append))
        

Nix

(use-package nix-mode
  :mode "\\.nix\\'")

Web dev

  • Helper utilities
    • Are we using nvm?
      (defun spook--nvm-p ()
        (when-let* ((node (string-trim (shell-command-to-string "fish -c 'readlink (which node)'")))
                    (nvm-bin-dir
                     (and (string-match-p "\/nvm\/" node)
                          (file-name-directory node))))
          nvm-bin-dir))
              
 (setq css-indent-offset spook--indent-width)

 (use-package js
   :mode "\\.js'"
   :ensure nil
   :config
   (setq js-indent-level spook--indent-width)
   :hook
   (((js-mode
      typescript-mode) . subword-mode)))

 (use-package web-mode
   :mode (("\\.html?\\'" . web-mode))
   :config
   (setq web-mode-markup-indent-offset spook--indent-width)
   (setq web-mode-code-indent-offset spook--indent-width)
   (setq web-mode-css-indent-offset spook--indent-width)
   (setq web-mode-content-types-alist '(("jsx" . "\\.js[x]?\\'"))))

 (use-package emmet-mode
   :hook ((html-mode       . emmet-mode)
	   (css-mode        . emmet-mode)
	   (js-mode         . emmet-mode)
	   (js-jsx-mode     . emmet-mode)
	   (typescript-mode . emmet-mode)
	   (typescript-tsx-mode . emmet-mode)
	   (web-mode        . emmet-mode))
   :config
   (setq emmet-insert-flash-time 0.001)	; effectively disabling it
   (add-hook 'js-jsx-mode-hook #'(lambda ()
				    (setq-local emmet-expand-jsx-className? t)))
   (add-hook 'typescript-tsx-mode-hook #'(lambda ()
					    (setq-local emmet-expand-jsx-className? t)))
   (add-hook 'web-mode-hook #'(lambda ()
				 (setq-local emmet-expand-jsx-className? t))))

 ;; Let's try flymake for js/ts since eglot likes it so much
 (use-package flymake-eslint)

 (spook--defkeymap
  "spook-flymake" "C-c e"
  '("n" . flymake-goto-next-error)
  '("p" . flymake-goto-prev-error)
  '("l" .  flymake-show-buffer-diagnostics)
  '("L" . flymake-show-project-diagnostics))

 (defun spook--setup-ts-js ()
   "Setup Javascript and Typescript for current buffer."
   ;; Add node_modules/.bin of current project to exec-path.
   (flycheck-mode -1)

   (if-let (nvm-bin (spook--nvm-p))
	(add-to-list 'exec-path nvm-bin)
     (let ((bin-dir
	     (expand-file-name
	      "node_modules/.bin/"
	      (locate-dominating-file default-directory "node_modules"))))
	(when (file-exists-p bin-dir)
	  (add-to-list 'exec-path bin-dir))))

   ;; TODO Remove this if flymake-eslint works well enough
   ;; Setup flycheck. It don't enable eslint for ts buffers for some reason
   ;; (setq-local
   ;;  flycheck-javascript-eslint-executable "eslint"
   ;;  flycheck-enabled-checkers '(javascript-eslint))
   ;; (flycheck-select-checker 'javascript-eslint)

   ;; For 95% of cases this is what I want
   (prettier-format-on-save-mode +1)
   (eglot-ensure)
   (setf flymake-eslint-project-root
	  (locate-dominating-file default-directory "package.json"))
   (flymake-eslint-enable))

 (setq js-mode-hook nil)
 (add-hook 'js-mode-hook #'spook--setup-ts-js)

 (use-package typescript-mode
   :mode "\\.ts?\\'"
   :hook ((typescript-mode . subword-mode))
   :init
   (define-derived-mode typescript-tsx-mode typescript-mode "tsx")  
   :config
   (setq-default typescript-indent-level spook--indent-width)
   (add-hook 'typescript-mode-hook #'spook--setup-ts-js))

 (use-package css-mode
   :ensure nil
   :mode "\\.s?css\\'")
  • Setup typescript-tsx-mode using code I don’t fully understand. Copied from typescript.el#4
    (use-package typescript-tsx-mode
      :ensure nil
      :mode (("\\.tsx\\'" . typescript-tsx-mode))
      :config
      (add-hook 'typescript-tsx-mode #'subword-mode))
    
    (use-package tree-sitter
      :hook ((typescript-mode . tree-sitter-hl-mode)
    	     (typescript-tsx-mode . tree-sitter-hl-mode)))
    
    (use-package tree-sitter-langs
      :after tree-sitter
      :config
      (tree-sitter-require 'tsx)
      (add-to-list 'tree-sitter-major-mode-language-alist '(typescript-tsx-mode . tsx)))
        
  • JSON support
    (use-package json-mode
      :mode "\\.json\\'")
        
  • Testing with jest
    (use-package jest)
        

Rust

(use-package rustic
  :init
  (setq rustic-cargo-bin "cargo")
  (push 'rustic-clippy flycheck-checkers))

Haskell

(use-package haskell-mode
  :mode "\\.hs\\'"
  :config
  (add-hook 'haskell-mode-hook #'subword-mode)

  (define-key haskell-mode-map (kbd "C-c , c") #'haskell-process-load-or-reload)
  (define-key haskell-mode-map (kbd "C-c , s") #'haskell-interactive-switch)
  (define-key haskell-mode-map (kbd "C-c , l") #'haskell-interactive-mode-clear)
  (define-key haskell-mode-map (kbd "C-c , T") #'haskell-doc-show-type)
  (define-key haskell-mode-map (kbd "C-c , t") #'haskell-mode-show-type-at))

Yaml

(use-package yaml-mode
  :mode "\\.ya?ml\\'")

Graphql

(use-package graphql-mode
  :mode "\\.graphql\\'")

Niceties

Nice to have features but not necessary.

  • Ace Jump for quickly jumping around in a buffer
    (spook--defkeymap
     "spook-jump" "C-c q"
     '("q" . ace-jump-mode)
     '("w" . ace-jump-word-mode))
    
    (use-package ace-jump-mode)
        
  • Treemacs for easy code exploration
    (use-package treemacs
      :ensure t
      :defer t
      :init
      (with-eval-after-load 'winum
        (define-key winum-keymap (kbd "M-0") #'treemacs-select-window))
      :config
      (progn
        (setq treemacs-collapse-dirs                   (if treemacs-python-executable 3 0)
              treemacs-deferred-git-apply-delay        0.5
              treemacs-directory-name-transformer      #'identity
              treemacs-display-in-side-window          t
              treemacs-eldoc-display                   'simple
              treemacs-file-event-delay                2000
              treemacs-file-extension-regex            treemacs-last-period-regex-value
              treemacs-file-follow-delay               0.2
              treemacs-file-name-transformer           #'identity
              treemacs-follow-after-init               t
              treemacs-expand-after-init               t
              treemacs-find-workspace-method           'find-for-file-or-pick-first
              treemacs-git-command-pipe                ""
              treemacs-goto-tag-strategy               'refetch-index
              treemacs-header-scroll-indicators        '(nil . "^^^^^^")
              treemacs-hide-dot-git-directory          t
              treemacs-indentation                     2
              treemacs-indentation-string              " "
              treemacs-is-never-other-window           nil
              treemacs-max-git-entries                 5000
              treemacs-missing-project-action          'ask
              treemacs-move-forward-on-expand          nil
              treemacs-no-png-images                   nil
              treemacs-no-delete-other-windows         t
              treemacs-project-follow-cleanup          nil
              treemacs-persist-file                    (expand-file-name ".cache/treemacs-persist" user-emacs-directory)
              treemacs-position                        'left
              treemacs-read-string-input               'from-child-frame
              treemacs-recenter-distance               0.1
              treemacs-recenter-after-file-follow      nil
              treemacs-recenter-after-tag-follow       nil
              treemacs-recenter-after-project-jump     'always
              treemacs-recenter-after-project-expand   'on-distance
              treemacs-litter-directories              '("/node_modules" "/.venv" "/.cask")
              treemacs-show-cursor                     nil
              treemacs-show-hidden-files               t
              treemacs-silent-filewatch                nil
              treemacs-silent-refresh                  nil
              treemacs-sorting                         'alphabetic-asc
              treemacs-select-when-already-in-treemacs 'move-back
              treemacs-space-between-root-nodes        t
              treemacs-tag-follow-cleanup              t
              treemacs-tag-follow-delay                1.5
              treemacs-text-scale                      nil
              treemacs-user-mode-line-format           nil
              treemacs-user-header-line-format         nil
              treemacs-wide-toggle-width               70
              treemacs-width                           35
              treemacs-width-increment                 1
              treemacs-width-is-initially-locked       t
              treemacs-workspace-switch-cleanup        nil)
    
        ;; The default width and height of the icons is 22 pixels. If you are
        ;; using a Hi-DPI display, uncomment this to double the icon size.
        ;;(treemacs-resize-icons 44)
    
        (treemacs-follow-mode t)
        (treemacs-filewatch-mode t)
        (treemacs-fringe-indicator-mode 'always)
        (when treemacs-python-executable
          (treemacs-git-commit-diff-mode t))
    
        (pcase (cons (not (null (executable-find "git")))
                     (not (null treemacs-python-executable)))
          (`(t . t)
           (treemacs-git-mode 'deferred))
          (`(t . _)
           (treemacs-git-mode 'simple)))
    
        (treemacs-hide-gitignored-files-mode nil))
      :bind
      (:map global-map
            ("M-0"       . treemacs-select-window)
            ("C-x t 1"   . treemacs-delete-other-windows)
            ("C-x t t"   . treemacs)
            ("C-x t d"   . treemacs-select-directory)
            ("C-x t B"   . treemacs-bookmark)
            ("C-x t C-t" . treemacs-find-file)
            ("C-x t M-t" . treemacs-find-tag)))
    
    (use-package treemacs-projectile
      :after (treemacs projectile)
      :ensure t)
    
    (use-package treemacs-magit
      :after (treemacs magit)
      :ensure t)
    
    (use-package treemacs-all-the-icons
      :config
      (treemacs-load-theme "all-the-icons"))
        
  • Highlight indentation
    (use-package highlight-indent-guides
      :config
      (setf highlight-indent-guides-method 'bitmap)
      (add-hook 'prog-mode-hook 'highlight-indent-guides-mode))
        
  • Move text around
    (use-package move-text
      :config
      (move-text-default-bindings))
        

Looks

(use-package doom-themes
  :config
  (setq doom-rouge-brighter-modeline t
        doom-rouge-brighter-comments t)
  ;; (load-theme 'doom-rouge t)
  )
(use-package nimbus-theme
  :config
  (load-theme 'nimbus t))

Modeline

(use-package doom-modeline
  :init
  (setq doom-modeline-height 24)
  (doom-modeline-mode 1))

Let’s also try smooth-scrolling.

(pixel-scroll-precision-mode t)

Applications

Non crucial things which should be loaded last. If they fail, nothing crucial is blocked.

  • Spell checking
    (with-eval-after-load "ispell"
      (setq ispell-program-name "hunspell")
      (setq ispell-dictionary "en_US,de_DE")
      (ispell-set-spellchecker-params)
      (ispell-hunspell-add-multi-dic "en_US,de_DE")
      (setq ispell-personal-dictionary "~/.emacs.d/.hunspell_per_dic"))
        
     (use-package flyspell
       :ensure nil
       :hook
       (text-mode . flyspell-mode)
       (prog-mode . flyspell-prog-mode)
       :config
       (define-key flyspell-mode-map (kbd "C-,") nil)
       (define-key flyspell-mode-map (kbd "C-.") nil)
       (define-key flyspell-mode-map (kbd "C-;") #'flyspell-correct-wrapper))
    
     (use-package flyspell-correct
       :after (flyspell)
       :commands (flyspell-correct-at-point
    		 flyspell-correct-wrapper))
        
  • Notes using denotes
     (setq denote-directory (expand-file-name "denotes" org-directory)
    	  denote-date-prompt-use-org-read-date t)
     (use-package denote
       :ensure (denote :type git
    			:host github
    			:repo "protesilaos/denote"
    			:branch "main")
       :config
       (add-hook 'dired-mode-hook #'denote-dired-mode))
        
    • Enhance denote a bit, don’t know why these aren’t a part of denote itself.
      (defun spook--denote-split-org-subtree (&optional prefix)
        "Create new Denote note as an Org file using current Org subtree."
        (interactive "P")
        (let ((text (org-get-entry))
              (heading (org-get-heading :no-tags :no-todo :no-priority :no-comment))
              (tags (org-get-tags))
              (subdir (when prefix (denote-subdirectory-prompt))))
          (delete-region (org-entry-beginning-position) (org-entry-end-position))
          (denote heading tags 'org subdir)
          (insert text)))
              
    • Setup for taking notes for reading/video-watching I do in Firefox.
      (defvar spook-notes-mode-map (make-sparse-keymap))
      
      (define-key spook-notes-mode-map (kbd "C-c i t") #'spook--insert-yt-ts-note)
      
      (define-minor-mode spook-notes-mode
        "Minor mode for taking spooky notes.
      It is used to set local keybindings depending on the kind of note
      being taken."
        :keymap spook-notes-mode-map)
      
      (defun spook--get-ff-yt-current-time ()
        "Return current time of youtube video running in Firefox's active tab."
        (spookfox-eval-js-in-active-tab
         (concat
          "(function () {"
          "try {"
          "const player = document.querySelector('.video-stream');"
          "return { time: player.currentTime, url: `${window.location.href}&t=${Math.floor(player.currentTime)}` };"
          "} catch(e) { return 0; }"
          "})()") t))
      
      (defun spook--insert-yt-ts-note (&optional url)
        "Insert note for current timestamp for URL in youtube.
      Inserted time is an org yt:// link to youtube video at that time."
        (interactive)
        (let* ((result (spook--get-ff-yt-current-time))
               (time (plist-get result :time))
               (url (string-replace "https" "yt" (plist-get result :url))))
          (insert (concat "- At [[" url "]["
                          (format-seconds "%m:%s" time)
                          "]]\n"))))
      
      (defun spook--url-equal-p (url1 url2)
        "Return t if URL1 and URL2 have same host, query and path."
        (let ((url1 (url-generic-parse-url url1))
              (url2 (url-generic-parse-url url2)))
          (and (equal (url-host url1)
                      (url-host url2))
               (equal (url-path-and-query url1)
                      (url-path-and-query url2)))))
      
      (defun spook--find-denote-for-ff-tab (url &optional subdir)
        "Find existing denote entry for firefox tab for URL in denote
      SUBDIR.
      If previously a note for URL was being taken, return that file;
       nil otherwise."
        (let ((case-fold-search t)
              (subdir (expand-file-name subdir denote-directory))
              (source-rx (rx "#+source: " (group (+ any) (not "#")))))
          (seq-find
           (lambda (file)
             (with-temp-buffer
               (insert-file-contents file)
               (search-forward-regexp source-rx nil t)
               (spook--url-equal-p url (string-trim (or (match-string 1) "")))))
           (mapcar
            (lambda (f) (expand-file-name f subdir))
            (cl-remove-if-not
             (lambda (f) (string-match-p ".org$" f))
             (directory-files subdir))))))
      
      (defun spook--denote-ff-tab ()
        "Create a new denote for current Firefox tab."
        (interactive)
        (let* ((tab (spookfox-request-active-tab))
               (url (plist-get tab :url))
               (yt-p (string-match-p "youtube.com" url))
               (tags '("reading"))
               (existing-denote (spook--find-denote-for-ff-tab url "reading")))
          (if existing-denote
              (find-file existing-denote)
            (when yt-p
              (push "video" tags))
      
            (denote (denote-title-prompt (plist-get tab :title))
                    tags "org" (expand-file-name "reading" denote-directory))
            (when yt-p (spook-notes-mode))
            (delete-region (point) (line-beginning-position 0))
            (insert (concat "#+source: " url "\n\n")))))
      
      (defun spook--micro-post ()
        "Quickly create a micro-post."
        (interactive)
        (let* ((body (read-from-minibuffer "Micro-Post body: ")) 
               (title (denote-title-prompt (concat (string-trim (substring body 0 (min (length body) 40)))
                                                   (when (> (length body) 40) "...")))))
          (denote title '("micro" "blog-post"))
          (delete-region (point) (line-beginning-position 0))
          (insert "#+published-on: ((mastodon . \"\"))\n\n")
          (insert body)))
              
    • CRM
      (defvar crm-directory (expand-file-name "crm" denote-directory))
      
      (defun spook-crm--open-or-create ()
        "Find or create CRM entry."
        (interactive)
        (let ((denote-directory crm-directory))
          (call-interactively #'denote-open-or-create)))
      
      (defun spook-crm--link-or-create ()
        "Find or create CRM entry."
        (interactive)
        (let ((denote-directory crm-directory))
          (call-interactively #'denote-link-or-create)))
              
    • Keyboard shortcuts for fluent note-taking/reading
      (spook--defkeymap
       "spook-notes" "C-c n"
       '("n" . denote-open-or-create)
       '("N" . denote-link-or-create)
       '("b" . denote-link-backlinks)
       '("d" . spook--diary-today)
       '("r" . spook--denote-ff-tab)
       '("p" . spook-crm--open-or-create)
       '("P" . spook-crm--link-or-create)
       '("m" . spook--micro-post))
              
    • Diary
      (defun spook--find-habit (title)
        "Find the habit with TITLE in current buffer."
        (cl-block 'spook--find-habit
          (org-map-entries
           (lambda ()
             (let ((el (org-element-at-point-no-context)))
               (when (and (seq-contains-p (org-get-tags el) "habit" #'equal)
                          (equal (downcase (org-element-property :raw-value el))
                                 (downcase title)))
                 (cl-return-from 'spook--find-habit el)))))))
      
      (defun spook--mark-habit-as-done (habit)
        "Mark HABIT as done."
        (with-current-buffer (find-file-noselect (expand-file-name "TODOs.org" org-directory))
          (org-mode)
          (let ((el (cl-case habit
                      (diary (spook--find-habit "write diary entry")))))
            (goto-char (org-element-property :begin el))
            (org-todo 'done))))
      
      (defun spook--diary-today ()
        "Go to today's diary entry."
        (interactive)
        (let ((denote-directory (expand-file-name "diary" denote-directory))
              (title (format-time-string "%Y-%m-%d")))
          (if-let ((file (seq-find
                          (lambda (f) (string-match-p title f))
                          (directory-files denote-directory))))
              (progn
                (find-file (expand-file-name file denote-directory))
                (goto-char (point-max)))
            (spook--mark-habit-as-done 'diary)
            (denote title '("diary")))))
      
              
    • Work notes
      (defun spook--workday-notes (prefix)
        "Go to work notes for today plus PREFIX days."
        (interactive "P")
        (let* ((days (if prefix (prefix-numeric-value prefix) 0))
               (denote-directory (expand-file-name "work" denote-directory))
               (date (time-add (current-time) (days-to-time days)))
               (title (format-time-string "%Y-%m-%d" date)))
          (if-let ((file (seq-find
                          (lambda (f) (string-match-p title f))
                          (directory-files denote-directory)))
                   (file (expand-file-name file denote-directory)))
              (progn
                (find-file file)
                ;; Remove any other denotes/work file from agenda
                ;; Assuming that this will always remove older workday files
                (setf org-agenda-files
                      (seq-filter
                       (lambda (file)
                         (not (string-match-p "denotes/work" file)))
                       org-agenda-files))
      
                (org-agenda-file-to-front file)
      
                (goto-char (point-max)))
            (denote title '("work") "org" nil title))))
      
      (spook--defkeymap
       "workday" "C-c n w"
       '("w" . spook--workday-notes)
       '("i" . on-issue-note-open-or-create)
       '("I" . on-issue-note-link-or-create))
              
  • dirvish for more powerful dired
    (use-package all-the-icons)
    (use-package dirvish
      :init
      (dirvish-override-dired-mode)
      :config
      (setq dirvish-attributes
            '(vc-state subtree-state all-the-icons collapse file-size))
    
      :bind
      (("C-c f" . dirvish-fd)
       :map dirvish-mode-map
       ("/"   . dirvish-narrow)
       ("a"   . dirvish-quick-access)
       ("f"   . dirvish-file-info-menu)
       ("y"   . dirvish-yank-menu)
       ("N"   . dirvish-narrow)
       ("^"   . dirvish-history-last)
       ("h"   . dirvish-history-jump) ; remapped `describe-mode'
       ("s"   . dirvish-quicksort)    ; remapped `dired-sort-toggle-or-edit'
       ("v"   . dirvish-vc-menu)      ; remapped `dired-view-file'
       ("TAB" . dirvish-subtree-toggle)
       ("M-f" . dirvish-history-go-forward)
       ("M-b" . dirvish-history-go-backward)
       ("M-l" . dirvish-ls-switches-menu)
       ("M-m" . dirvish-mark-menu)
       ("M-t" . dirvish-layout-toggle)
       ("M-s" . dirvish-setup-menu)
       ("M-e" . dirvish-emerge-menu)
       ("M-j" . dirvish-fd-jump)))
        
  • Ledger
    (use-package ledger-mode
      :mode "\\.ledger\\'"
      :config
      (setq ledger-default-date-format ledger-iso-date-format))
        
  • spookfox
    (when (file-exists-p "~/Documents/work/spookfox")
      (use-package spookfox
        :ensure (spookfox :type git
                          :repo "~/Documents/work/spookfox"
                          :files ("lisp/*.el" "lisp/apps/*.el"))
        :config
        (setq spookfox-enabled-apps (list spookfox-jscl spookfox-tabs spookfox-js-injection))
        (setq spookfox-saved-tabs-target
              `(file+headline ,(expand-file-name "spookfox.org" org-directory) "Tabs"))
        (spookfox-init))
    
      (defun spook--switch-tab-and-focus ()
        "Switch to browser tab and bring browser in focus."
        (interactive)
        (spookfox-switch-tab)
        (when (eq 'darwin system-type)
          (ns-do-applescript "tell application \"Firefox\"\n\tactivate\n\tend tell")))
    
      (define-key spook-buffers-keymap (kbd "t") #'spook--switch-tab-and-focus))
        
  • saunf

    Use the local repo; very risky, should change.

    (when (file-exists-p (expand-file-name "~/Documents/work/saunf"))
      (use-package saunf
        :after sly
        :ensure (saunf :type git
                       :repo "~/Documents/work/saunf"
                       :files ("src/saunf.el"))))
        
  • org-noter
    (use-package nov
      :mode ("\\.epub\\'" . nov-mode))
    ;; (use-package org-noter) 
        
  • Shelldon

    Let’s try replacing alacritty with async-shell-command

    (use-package shelldon
      :ensure (shelldon :type git
                        :host github
                        :repo "Overdr0ne/shelldon"
                        :branch "master"
                        :files ("shelldon.el"))
      :config
      (setq shell-command-switch "-ic")
      (add-hook 'shelldon-mode-hook 'ansi-color-for-comint-mode-on)
      (add-to-list 'comint-output-filter-functions 'ansi-color-process-output)
      (autoload 'ansi-color-for-comint-mode-on "ansi-color" nil t)
    
      (global-set-key (kbd "M-s") #'shelldon)
      (global-set-key (kbd "M-S") #'shelldon-loop))
        

    Shelldon recommends installing bash-complete.

    (use-package bash-completion
      :config
      (autoload 'bash-completion-dynamic-complete
        "bash-completion"
        "BASH completion hook")
      (add-hook 'shell-dynamic-complete-functions
                'bash-completion-dynamic-complete))
        
    • Enable listing shelldon buffers.

      Shelldon hides its buffers as soon as output window is hidden. That is fine for one-off commands, but I also run long-running commands like dev-servers etc, which need to be closed manually and also need to check the output for errors.

      (defvar shell-output-history nil)
      (defun spook--switch-shell-output ()
        "Select shelldon output buffers."
        (interactive)
        (consult-buffer
         (list
          `(:name "Shell Output"
            :narrow 98
            :category buffer
            :face consult-buffer
            :history shell-output-history
            :state consult--buffer-state
            :default t
            :items
            (lambda ()
              (consult--buffer-query
               :exclude nil
               :include "shelldon"
               :as #'buffer-name))))))
      
      (define-key spook-buffers-keymap (kbd "o") #'spook--switch-shell-output)
              
    • Quick helper to delete all shelldon buffers, because sometimes there are a lot of them. It is hard to delete them because they are hidden and don’t show up in ibuffer
      (defun spook--delete-all-shelldon-buffers ()
        (interactive)
        (cl-dolist (buf (cl-remove-if-not
                         (lambda (buf) (s-contains-p "*shelldon" (buffer-name buf)))
                         (buffer-list)))
          (kill-buffer buf)))
              
  • Irc

    Small utility to quickly connect to irc.

    ;; (setq
    ;;  erc-nick "bitspook"
    ;;  erc-password (auth-source-pass-get 'secret "libera.chat/bitspook")
    ;;  erc-autojoin-channels-alist '(("libera.chat" "#commonlisp" "#emacs" "#emacs-berlin" "#clschool" "#whereiseveryone" "#lispcafe")))
        
  • Terraform
    (use-package terraform-mode)
        
  • Eww
    (add-hook
     'eww-mode-hook
     (lambda ()
       (display-line-numbers-mode -1)))
        
  • org-download to easily attach images in my notes
    (use-package org-download
      :init
      (setq-default org-download-method 'attach
                    org-download-image-dir (expand-file-name "data" org-directory))
      :config
      ;; Drag-and-drop to `dired`
      (add-hook 'dired-mode-hook 'org-download-enable))
        

Private work related config

(elpaca-wait)
(let ((private-config (expand-file-name "./private.el" user-emacs-directory)))
  (when (file-exists-p private-config)
    (load-file private-config)))

About

My Emacs configuration