Skip to content

Latest commit

 

History

History
1083 lines (782 loc) · 40 KB

Emacs.org

File metadata and controls

1083 lines (782 loc) · 40 KB

Emacs Configuration

19:12 <alternateved> couple of helpful settings: 19:12 <alternateved> (completions-format ‘one-column) 19:13 <alternateved> (completions-max-height 20) 19:13 <alternateved> (completion-auto-select ‘second-tab)

Preface

Welcome to the latest iteration of my Emacs configuration! I’m not sure how many times I’ve rewritten my Emacs configuration over the years (it’s probably less than you think), but I’m approaching this one with some specific goals and use cases in mind.

Goals

I have specific goals for this configuration which I hope will serve some personal use cases and also inspire other people to experiment with their own ideas:

  • The base configuration is terminal-first, meaning that everything I add there must provide a good experience for Emacs in the terminal.
  • init.el itself should have no external package dependencies (if possible) and I should be able to drop it on any machine using curl and have a comfortable environment ready to go immediately.
  • Additional functionality will be introduced using optional configuration modules which are only loaded on specific machines.
  • Use built-in Emacs functionality as much as I can, only pull in community packages for critical workflows.
  • Avoid special convenience key bindings; stay as close to “vanilla” as I can.

Why?

This particular set of goals may seem surprising, so I’d like to provide some rationale. I’m not attempting this because I think it’s somehow better to be minimalistic. I am trying to address specific use cases that I have instead of trying to come up with a generic configuration structure. What I do here may not be right for you!

I’ve described in a few previous live streams how I feel that it’s important I understand fundamental Emacs functionality and key bindings so that I can teach them effectively. I really want to give the best advice to someone on what options they have available for their workflow and I can’t do that if I don’t understand the tools that Emacs provides by default.

Also, I think there is some value in writing your own Emacs Lisp code for certain things that may be provided by community packages. Why depend on a small package with configuration options to suit many users when you could just write something similar for yourself that is tailored specifically to your needs?

There is probably more to say here that I haven’t thought of yet; please feel free to ask me any questions you have on IRC, the forum, or via email (my first name at systemcrafters.net).

Built-in vs Community Packages

A short list of features I’m trying to stick with this time before reaching for community packages:

  • Use icomplete-vertical-mode or fido-vertical-mode instead of Vertico. Vertico is amazing, but I want to know what the actual pain points of the built-in solutions are before I resort to using something that is not built in.
  • Get used to the built-in completion-styles before I try pulling in orderless. orderless is excellent but I honestly have not experimented enough with the built-in styles to see if I can get around well enough with them.

p

  • Use vc-mode instead of Magit as long as I can. vc-mode is nowhere near as polished as Magit, by any means. However, I should be able to do many of the same operations I accomplish with Magit (aside from interactive rebase?). I want to test the limits of vc-mode before I pull in Magit, and even then, I may only use Magit for specific tasks that vc-mode is not good at.
  • Try to avoid using consult for as long as I can. consult-line is pretty useful, but isearch is mostly fine and I haven’t even tried to configure it to be nicer yet. The one thing I will miss from consult is consult-ripgrep, but I need to see if there’s a way to use ripgrep directly with Emacs’ existing grep functionality.
  • Try to avoid using “workspace” packages like beframe or tabspaces. There must be other useful ways to manage buffers that don’t require forcing per-project isolation. There’s already C-x p b (project-switch-to-buffer) that could take care of 80% of my needs. I also haven’t experimented enough with ibuffer for mass buffer management to clean up those that I don’t need any more in my current session.
  • No configuration helpers other than use-package since it’s now built in with Emacs 30. There are also new keymap macros like define-keymap which remove the need for packages like general.el, etc.

Back to Literate Configuration

You may have realized that I am back to using a literate Emacs configuration with Org Babel. I’ve actually been planning to do this for a while but never got around to it until now.

I had a couple of reasons for abandoning my old literate configuration:

  • Tangling config changes after syncing from my dotfiles can be a pain
  • I wanted to treat my configuration as real code instead of code blocks in an Org file

After spending over a year with my Emacs config as plain .el files, I feel like I’m missing the convenience of one big file that contains everything in one place. Sure, I can move around pretty quickly in my dotfiles repo using C-x p f (project-find-file), but half the time I forget which file some aspect of my Emacs config lives in.

Also, as many have noticed, the website where I publish my configuration has been down for almost as long as I haven’t been using a literate config due to DNS changes at SourceHut. I’ve been thinking about how I can get a website back up for my config again and tried a few different options, but going back to literate just feels like the most appropriate solution for now.

Lastly, I think a literate config is superior because it enables one to write real prose to explain their configuration and the rationale for why they do things a certain way. I wouldn’t have bothered to write this many paragraphs as a comment in an Emacs Lisp file. People can learn from reading about how you use Emacs, it’s a benefit to the community!

Installation

To set up this configuration on a new machine, there are two options:

  • Download emacs/init.el into your home folder as .emacs
  • OR Clone the whole dotfiles repo to ~/.dotfiles and run the following commands:
mkdir ~/emacs.d
ln -sf ~/.dotfiles/emacs/init.el ~/.emacs.d/

We don’t actually symlink the whole ~/.dotfiles/emacs folder to ~/.emacs.d to prevent Emacs from dumping runtime files and ELPA packages into the dotfiles repository!

init.el

The standard init.el file is shared between all machines that use this configuration. It is intended that file can be copied directly onto a new machine to get a convenient working environment up and running quickly.

Additional functionality is loaded from module files which are defined in sections later in this document.

I’ve structured all of this in a very concise way because much of it doesn’t need explanation if you’ve been using Emacs for a while.

Base Configuration

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

;;; This file is generated from the Emacs.org file in my dotfiles repository!

;;; ----- Basic Configuration -----

;; Core settings
(setq ;; Flash the UI instead of beeping
      visible-bell t

      ;; Yes, this is Emacs
      inhibit-startup-message t

      ;; Instruct auto-save-mode to save to the current file, not a backup file
      auto-save-default nil

      ;; No backup files, please
      make-backup-files nil

      ;; Make it easy to cycle through previous items in the mark ring
      set-mark-command-repeat-pop t

      ;; Don't warn on large files
      large-file-warning-threshold nil

      ;; Follow symlinks to VC-controlled files without warning
      vc-follow-symlinks t

      ;; Don't warn on advice
      ad-redefinition-action 'accept

      ;; Revert Dired and other buffers
      global-auto-revert-non-file-buffers t

      ;; Silence compiler warnings as they can be pretty disruptive
      native-comp-async-report-warnings-errors nil)

;; Core modes
(repeat-mode 1)                ;; Enable repeating key maps
(menu-bar-mode 0)              ;; Hide the menu bar
(tool-bar-mode 0)              ;; Hide the tool bar
(savehist-mode 1)              ;; Save minibuffer history
(scroll-bar-mode 0)            ;; Hide the scroll bar
(xterm-mouse-mode 1)           ;; Enable mouse events in terminal Emacs
(display-time-mode 1)          ;; Display time in mode line / tab bar
(fido-vertical-mode 1)         ;; Improved vertical minibuffer completions
(column-number-mode 1)         ;; Show column number on mode line
(tab-bar-history-mode 1)       ;; Remember previous tab window configurations
(auto-save-visited-mode 1)     ;; Auto-save files at an interval
(global-visual-line-mode 1)    ;; Visually wrap long lines in all buffers
(global-auto-revert-mode 1)    ;; Refresh buffers with changed local files

;; Tabs to spaces
(setq-default indent-tabs-mode nil
	            tab-width 2)

;; Display line numbers in programming modes
(add-hook 'prog-mode-hook #'display-line-numbers-mode)

;; Make icomplete slightly more convenient
(keymap-set icomplete-fido-mode-map "M-h" 'icomplete-fido-backward-updir)
(keymap-set icomplete-fido-mode-map "TAB" 'icomplete-force-complete)

;; Delete trailing whitespace before saving buffers
(add-hook 'before-save-hook 'delete-trailing-whitespace)

;; Move customization settings out of init.el
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(when (file-exists-p custom-file)
  (load custom-file t))

;; Match completion substrings that may be out of order
(defun dw/override-fido-completion-styles ()
  (setq-local completion-styles '(substring partial-completion emacs22)))

(add-hook 'icomplete-minibuffer-setup-hook 'dw/override-fido-completion-styles)

System Identification

I often need to modify behavior based on the type of system Emacs is running on, especially if it’s running on Guix.

;;; ----- System Identification -----

(defvar dw/is-termux
  (string-suffix-p "Android" (string-trim (shell-command-to-string "uname -a"))))

(defvar dw/current-distro (or (and (eq system-type 'gnu/linux)
                                   (file-exists-p "/etc/os-release")
                                   (with-temp-buffer
                                     (insert-file-contents "/etc/os-release")
                                     (search-forward-regexp "^ID=\"?\\(.*\\)\"?$")
                                     (intern (or (match-string 1)
                                                 "unknown"))))
                              'unknown))

(defvar dw/is-guix-system (eql dw/current-distro 'guix))

Package Management

Customize package management based on the type of system Emacs is running on.

;;; ----- Package Management -----

;; Automatically install packages (when not on Guix) but don't load
;; them until requested
(setq use-package-always-ensure (not dw/is-guix-system)
      use-package-always-defer t)

Configuration Management

This section is currently under development as I figure out the best pattern to use for providing customization “knobs” on the base configuration.

For now, the idea is that I provide variables and functions that can be called in machine-specific configuration files (named with the system-name) to customize basic configuration details and load extension modules that are needed for each machine.

;;; ----- Configuration Management -----

(defvar dw/use-config-modules '()
  "A list of module symbols to load once init.el is finished.")

(defvar dw/common-config-modules '(dw-auth
                                   dw-irc
                                   dw-present
                                   dw-0x0
                                   dw-writing
                                   dw-workflow)
  "Configuration modules most commonly used across my machines.")

;; Add configuration modules to load path
(add-to-list 'load-path '"~/.dotfiles/emacs/modules")

;; Load system-specific configuration
(let ((config-path
       (format "~/.dotfiles/emacs/systems/%s.el" system-name)))
  (if (file-exists-p config-path)
      (load-file config-path)
    (message "No per-system configuration found for %s!" system-name)))

Appearance

I prefer to use terminals that support the full range of colors so that themes like ef-themes can have an equivalent appearance to graphical Emacs.

However, setting a background color in an Emacs theme generally defeats any transparency settings of the terminals I’ve used so I’ve added the dw/clear-background-color function to clear the background color after a theme gets applied.

I may switch to using a highly-customized modus-vivendi here if I can figure out the right combination of colors to approximate the usual doom-palenight theme that I use on System Crafters videos and streams.

;;; ----- Appearance -----

(defun dw/set-terminal-title (title)
  (send-string-to-terminal (format "\e]0;%s\a" title)))

(defun dw/clear-background-color (&optional frame)
  (interactive)
  (or frame (setq frame (selected-frame)))
  "unsets the background color in terminal mode"
  (unless (display-graphic-p frame)
    ;; Set the terminal to a transparent version of the background color
    (send-string-to-terminal
     (format "\033]11;[90]%s\033\\"
         (face-attribute 'default :background)))
    (set-face-background 'default "unspecified-bg" frame)))

;; Clear the background color for transparent terminals
(unless (display-graphic-p)
  (add-hook 'after-make-frame-functions 'dw/clear-background-color)
  (add-hook 'window-setup-hook 'dw/clear-background-color)
  (add-hook 'ef-themes-post-load-hook 'dw/clear-background-color))

(when (display-graphic-p)
  (set-face-attribute 'default nil
                      :font "JetBrains Mono"
                      :weight 'normal
                      :height 140)

  ;; Set the fixed pitch face
  (set-face-attribute 'fixed-pitch nil
                      :font "JetBrains Mono"
                      :weight 'normal
                      :height 140)

  ;; Set the variable pitch face
  (set-face-attribute 'variable-pitch nil
                      :font "Iosevka Aile"
                      :height 120
                      :weight 'normal)

  ;; Make frames transparent
  (set-frame-parameter (selected-frame) 'alpha-background 93)
  (add-to-list 'default-frame-alist '(alpha-background . 93))
  (set-frame-parameter (selected-frame) 'fullscreen 'maximized)
  (add-to-list 'default-frame-alist '(fullscreen . maximized)))

(use-package modus-themes
  :ensure nil
  :demand t
  :custom
  (modus-themes-italic-constructs t)
  (modus-themes-bold-constructs t)
  (modus-themes-common-palette-overrides
   `((bg-main "#292D3E")
     (bg-active bg-main)
     (fg-main "#EEFFFF")
     (fg-active fg-main)
     (fringe unspecified)
     (border-mode-line-active unspecified)
     (border-mode-line-inactive unspecified)
     (fg-mode-line-active "#A6Accd")
     (bg-mode-line-active "#232635")
     (fg-mode-line-inactive "#676E95")
     (bg-mode-line-inactive "#282c3d")
     (bg-tab-bar      "#242837")
     (bg-tab-current  bg-main)
     (bg-tab-other    bg-active)
     (fg-prompt "#c792ea")
     (bg-prompt unspecified)
     (bg-hover-secondary "#676E95")
     (bg-completion "#2f447f")
     (fg-completion white)
     (bg-region "#3C435E")
     (fg-region white)

     (fg-heading-0 "#82aaff")
     (fg-heading-1 "#82aaff")
     (fg-heading-2 "#c792ea")
     (fg-heading-3 "#bb80b3")
     (fg-heading-4 "#a1bfff")

     (fg-prose-verbatim "#c3e88d")
     (bg-prose-block-contents "#232635")
     (fg-prose-block-delimiter "#676E95")
     (bg-prose-block-delimiter bg-prose-block-contents)

     (accent-1 "#79a8ff")

     (keyword "#89DDFF")
     (builtin "#82aaff")
     (comment "#676E95")
     (string "#c3e88d")
     (fnname "#82aaff")
     (type "#c792ea")
     (variable "#ffcb6b")
     (docstring "#8d92af")
     (constant "#f78c6c")))
  :init
  (load-theme 'modus-vivendi-tinted t)
  (add-hook 'modus-themes-after-load-theme-hook #'dw/clear-background-color))

;; Make vertical window separators look nicer in terminal Emacs
(set-display-table-slot standard-display-table 'vertical-border (make-glyph-code ?│))

;; Clean up the mode line
(setq-default mode-line-format
              '("%e" "  "
                (:propertize
                 ("" mode-line-mule-info mode-line-client mode-line-modified mode-line-remote))
                mode-line-frame-identification
                mode-line-buffer-identification
                "   "
                mode-line-position
                mode-line-format-right-align
                "  "
                (project-mode-line project-mode-line-format)
                " "
                (vc-mode vc-mode)
                "  "
                mode-line-modes
                mode-line-misc-info
                "  ")
              project-mode-line t
              mode-line-buffer-identification '(" %b")
              mode-line-position-column-line-format '(" %l:%c"))

Tab Bar Appearance

Tweak the tab bar to remove some unnecessary elements and shift the global-mode-string there.

;; Move global mode string to the tab-bar and hide tab close buttons
(setq tab-bar-close-button-show nil
      tab-bar-separator " "
      tab-bar-format '(tab-bar-format-menu-bar
                       tab-bar-format-tabs-groups
                       tab-bar-separator
                       tab-bar-format-align-right
                       tab-bar-format-global))

;; Turn on the tab-bar
(tab-bar-mode 1)

Display Time and World Clock

Time is relative, OK?

;; Customize time display
(setq display-time-load-average nil
      display-time-format "%l:%M %p %b %d W%U"
      display-time-world-time-format "%a, %d %b %I:%M %p %Z"
      display-time-world-list
      '(("Etc/UTC" "UTC")
        ("Europe/Athens" "Athens")
        ("America/Los_Angeles" "Seattle")
        ("America/Denver" "Denver")
        ("America/New_York" "New York")
        ("Pacific/Auckland" "Auckland")
        ("Asia/Shanghai" "Shanghai")
        ("Asia/Kolkata" "Hyderabad")))

Send Special Buffers to a Popup Window

Here’s another minimal implementation of a package I commonly use called popper.el. The goal here is to automatically place a specific set of buffers into a popup window at the bottom of the frame and make that window togglable with a key binding.

I don’t currently support buffer cycling like popper does but I don’t think I used it that much to begin with.

;; ----- Special Buffers as Popup Window -----

(setq display-buffer-alist
      '(("\\*\\(shell\\|.*term\\|.*eshell\\|help\\|compilation\\|Async Shell Command\\|Occur\\|xref\\).*\\*"
        (display-buffer-reuse-window display-buffer-in-side-window)
        (side . bottom)                  ; Popups go at the bottom
        (slot . 0)                       ; Use the first slot at the bottom
        (post-command-select-window . t) ; Select the window upon display
        (window-height . 0.3))))         ; 30% of the frame height

(defun dw/toggle-popup-window ()
  (interactive)
  (if-let ((popup-window
            (get-window-with-predicate
             (lambda (window)
               (eq (window-parameter window 'window-side)
                   'bottom)))))

      ;; Focus the window if it is not selected, otherwise close it
      (if (eq popup-window (selected-window))
          (delete-window popup-window)
        (select-window popup-window))

    ;; Find the most recent buffer that matches the rule and show it
    ;; NOTE: This logic is somewhat risky because it makes the assumption
    ;;       that the popup rule comes first in `display-buffer-alist'.
    ;;       I chose to do this because maintaining a separate variable
    ;;       for this rule meant I had to re-evaluate 2 different forms
    ;;       to update my rule list.
    (if-let ((popup-buffer
              (seq-find (lambda (buffer)
                          (buffer-match-p (caar display-buffer-alist)
                                          (buffer-name buffer)))
                        (if (project-current)
                            (project-buffers (project-current))
                          (buffer-list (selected-frame))))))
        (display-buffer popup-buffer (cdar display-buffer-alist))
      (message "No popup buffers found."))))

;; TODO: This binding may need to change
(keymap-global-set "C-c p" #'dw/toggle-popup-window)
(with-eval-after-load 'term
  (keymap-set term-raw-map "C-c p" #'dw/toggle-popup-window))

Essential Org Mode Configuration

Here are the most important Org Mode settings that enable me to edit files comfortably, especially my literate configuration files.

;;; ----- Essential Org Mode Configuration -----

(setq org-ellipsis ""
      org-startup-folded 'content
      org-cycle-separator-lines 2
      org-fontify-quote-and-verse-blocks t)

;; Indent org-mode buffers for readability
(add-hook 'org-mode-hook #'org-indent-mode)

;; Set up Org Babel languages
(org-babel-do-load-languages
 'org-babel-load-languages
 '((emacs-lisp . t)
   (shell . t)))

;; Use org-tempo
(use-package org-tempo
  :ensure nil
  :demand t
  :config
  (dolist (item '(("sh" . "src sh")
                  ("el" . "src emacs-lisp")
                  ("li" . "src lisp")
                  ("sc" . "src scheme")
                  ("ts" . "src typescript")
                  ("py" . "src python")
                  ("yaml" . "src yaml")
                  ("json" . "src json")
                  ("einit" . "src emacs-lisp :tangle emacs/init.el")
                  ("emodule" . "src emacs-lisp :tangle emacs/modules/dw-MODULE.el")))
    (add-to-list 'org-structure-template-alist item)))

Document Centering

I previously used visual-fill-column-mode for this functionality but decided to write my own minimal implementation so that I could avoid installing a MELPA package.

Works pretty well, but I’m not fully convinced this needs to be in init.el. It certainly does make the writing experience more pleasant but may not be critical for minimal config deployments.

;;; ----- Document Centering -----

(defvar center-document-desired-width 90
  "The desired width of a document centered in the window.")

(defun center-document--adjust-margins ()
  ;; Reset margins first before recalculating
  (set-window-parameter nil 'min-margins nil)
  (set-window-margins nil nil)

  ;; Adjust margins if the mode is on
  (when center-document-mode
    (let ((margin-width (max 0
			     (truncate
			      (/ (- (window-width)
				    center-document-desired-width)
				 2.0)))))
      (when (> margin-width 0)
	(set-window-parameter nil 'min-margins '(0 . 0))
	(set-window-margins nil margin-width margin-width)))))

(define-minor-mode center-document-mode
  "Toggle centered text layout in the current buffer."
  :lighter " Centered"
  :group 'editing
  (if center-document-mode
      (add-hook 'window-configuration-change-hook #'center-document--adjust-margins 'append 'local)
    (remove-hook 'window-configuration-change-hook #'center-document--adjust-margins 'local))
  (center-document--adjust-margins))

(add-hook 'org-mode-hook #'center-document-mode)

Shells

This is a configuration for Emacs’ built in shells, term / ansi-term and eshell.

;; Coming soon.

Dired

Dired doesn’t need much configuration, but the following just ensures that Dired buffers are organized in a way that makes sense to me, are free from unneeded information (at first), and doesn’t leave a ton of buffers open as I move around.

I also add a binding for b to dired-up-directory because it seems very strange to me that they have f bound to dired-find-file without b moving in the “opposite” direction.

;;; ----- Dired -----

(defun dw/dired-mode-hook ()
  (interactive)
  (dired-hide-details-mode 1)
  (hl-line-mode 1))

(use-package dired
  :ensure nil
  :bind (:map dired-mode-map
              ("b" . dired-up-directory))
  :config
  (setq dired-listing-switches "-alv --group-directories-first"
        dired-omit-files "^\\.[^.].*"
        dired-omit-verbose nil
        dired-dwim-target 'dired-dwim-target-next
        dired-hide-details-hide-symlink-targets nil
        dired-kill-when-opening-new-dired-buffer t
        delete-by-moving-to-trash t)

  (add-hook 'dired-mode-hook #'dw/dired-mode-hook))

Proced

The built-in proced package is quite useful for producing a list of all running processes. It’s basically like having top built in to Emacs.

This configuration makes the proced buffer update automatically on an interval so that you can watch process activity as it changes.

(use-package proced
  :ensure nil
  :config
  (setq proced-auto-update-interval 1)
  (add-hook 'proced-mode-hook
            (lambda ()
              (proced-toggle-auto-update 1))))

Searching

I search through files a lot. Here are some tweaks to make that experience a bit faster and more convenient:

;; Make sure ripgrep is used everywhere
(setq xref-search-program 'ripgrep
      grep-command "rg -nS --noheading")

Finalization

At the end of init.el, we finalize any configuration settings that may have been applied at the per-system level. This includes loading any configuration modules that are requested.

;;; ----- Finalization

;; Load requested configuration modules
(dolist (module dw/use-config-modules)
  (require module))

Modules

The following sections contain optional modules that will be loaded on a per-system basis depending on whether the specified features are needed.

dw-writing - Writing Tools

I use Denote as my primary tool for writing in Emacs. It enables me to write whatever I need very efficiently without thinking too much about where the information belongs.

I also use it as the core for my personal productivity system which revolves largely around Denote-managed Org Mode notes with special file-level tags.

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

(use-package denote
  :demand t
  :bind (("C-c n l" . denote-link-or-create)
         ("C-c n o" . denote-open-or-create)
         ("C-c n r" . denote-rename-file-using-front-matter))
  :custom
  (denote-directory "~/Notes")
  (denote-rename-buffer-format "Denote: %t (%k)")
  (denote-infer-keywords nil)
  (denote-known-keywords
   '("pra" "prb" "prc"
     "ply" "plm" "plw"
     "kt" "ke" "kp" "kl" "ka" "kap"
     "kcp" "kca" "kcc"
     "kra" "krb" "krv"
     "rn"))

  :config

  (require 'denote-rename-buffer)
  (require 'denote-org-extras)

  ;; Rename buffers with the note name
  (denote-rename-buffer-mode 1)

  ;; Buttonize all denote links in text buffers
  (add-hook 'text-mode-hook #'denote-fontify-links-mode-maybe))

(defun dw/setup-markdown-mode ()
  (center-document-mode 1)
  (display-line-numbers-mode 0))

(use-package markdown-mode
  :config
  (setq markdown-command "marked")
  (add-hook 'markdown-mode-hook #'dw/setup-markdown-mode)
  (dolist (face '((markdown-header-face-1 . 1.2)
                  (markdown-header-face-2 . 1.1)
                  (markdown-header-face-3 . 1.0)
                  (markdown-header-face-4 . 1.0)
                  (markdown-header-face-5 . 1.0)))
    (set-face-attribute (car face) nil :weight 'normal :height (cdr face))))

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

(use-package howm
  :init
  (setq howm-directory "~/Howm")
  (setq howm-home-directory howm-directory)
  ;(setq howm-file-name-format "%Y-%m-%d-%H%M%S.org")
  ;(setq howm-view-title-header "*")
  ;(setq howm-dtime-format "<%Y-%m-%d %a %H:%M>")
  (setq howm-file-name-format "%Y-%m-%d-%H%M%S.md")
  (setq howm-view-title-header "#")
  (setq howm-prefix (kbd "C-c ;"))
  :bind*
  ;; Conveniently open the Howm menu with "C-c ; ;".
  ("C-c ; ;" . howm-menu))

(provide 'dw-writing)

dw-workflow - Workflow, Task, and Project Management

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

;;; ----- TODO Configuration -----

(setq org-todo-keywords
      '((sequence "TODO(t)" "WAIT(w)" "|" "DONE(d!)")))

(setq org-todo-keyword-faces
      '(("GOAL" . (:foreground "orange red" :weight bold))
        ("WAIT" . (:foreground "HotPink2" :weight bold))
        ("BACK" . (:foreground "MediumPurple3" :weight bold))))

;;; ----- Context Tags -----

(setq-default org-tag-alist
              '((:startgroup)
                ("Areas")
                (:grouptags)
                ("@home" . ?H)
                ("@work" . ?W)
                (:endgroup)

                (:startgrouptag . nil)
                ("Contexts")
                (:grouptags)
                ("@computer" . ?C)
                ("@mobile" . ?M)
                ("@calls" . ?A)
                ("@errands" . ?E)
                (:endgrouptag)

                ;; Task Types
                (:startgrouptag . nil)
                ("Types")
                (:grouptags)
                ("@easy" . ?e)
                ("@hacking" . ?h)
                ("@writing" . ?w)
                ("@creative" . ?v)
                ("@accounting" . ?a)
                ("@email" . ?m)
                ("@system" . ?s)
                (:endgrouptag)

                ;; Workflow states
                (:startgroup . nil)
                ("States")
                (:grouptags)
                ("@plan" . ?p)
                ("@review" . ?r)
                ("@followup" . ?f)
                (:endgroup)))


;; Only make context tags inheritable (what about noexport?)
(setq org-use-tag-inheritance "^@")

;;; ----- Time Tracking -----

;; Clock in on the current task when setting a timer
(add-hook 'org-timer-set-hook #'org-clock-in)

;; Clock out of the current task when the timer is complete
(add-hook 'org-timer-done-hook #'org-clock-out)

;;; ----- Agenda Configuration -----

(defvar dw/base-agenda-files '("Inbox.org" "Schedule.org")
  "The base agenda files that will always be included.")

(setq org-agenda-span 'day
      org-agenda-start-with-log-mode t
      org-agenda-files dw/base-agenda-files
      org-agenda-window-setup 'current-window)

;; Make done tasks show up in the agenda log
(setq org-log-done 'time
      org-log-into-drawer t)

;;; ----- Denote Integration -----

(defun dw/refresh-agenda-files ()
  (interactive)
  (setq org-agenda-files
        (append (denote-directory-files "_pra")
                dw/base-agenda-files)))

(defun dw/goto-weekly-note ()
  (interactive)
  (let* ((note-title (format-time-string "%Y-W%V"))
         (existing-notes
          (denote-directory-files (format "-%s" note-title) nil t)))
    (if existing-notes
        (find-file (car existing-notes))
      (denote note-title '("plw")))))

(with-eval-after-load 'denote
  ;; Quick access commands
  (keymap-set global-map "C-c n w" #'dw/goto-weekly-note)

  ;; Refresh agenda files the first time
  (dw/refresh-agenda-files)

  ;; Update agenda files after notes are created or renamed
  (add-hook 'denote-after-rename-file-hook #'dw/refresh-agenda-files)
  (add-hook 'denote-after-new-note-hook #'dw/refresh-agenda-files))

(provide 'dw-workflow)

dw-present - Presentations and Live Streaming

I’m planning to develop a new presentation configuration using Prot’s Logos package because it’s a bit more flexible and will enable me to treat code files as presentations in addition to Org files and pretty much any other type of file that has syntax that can be interpreted as a page marker (like a comment string).

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

(defun dw/present-prepare-slide ()
  (when (and logos-focus-mode
             (derived-mode-p 'org-mode))
    (org-overview)
    (org-show-entry)
    (org-show-children)))

(defun dw/present-toggle ()
  "Configures the buffer for a presentation."
  (interactive)
  (if logos-focus-mode
      (progn
        (setq-local face-remapping-alist nil)
        (widen)
        (logos-focus-mode 0))

    (setq-local face-remapping-alist '((default (:height 1.5) default)
                                       (org-document-title (:height 1.75) org-document-title)
                                       (org-block-begin-line (:height 0.7) org-block)))

    ;; Narrow the buffer and start focus mode
    (logos-narrow-dwim)
    (logos-focus-mode 1)

    ;; Prepare the slide
    (dw/present-prepare-slide)))

(use-package logos
  :bind (([remap narrow-to-region] . logos-narrow-dwim)
	       ([remap forward-page] . logos-forward-page-dwim)
         ([remap backward-page] . logos-backward-page-dwim))
  :custom
  (logos-outlines-are-pages t)
  (logos-scroll-lock t)
  :config
  (setf (alist-get 'org-mode logos-outline-regexp-alist) "^\\*\\{1,2\\} +")
  (add-hook 'logos-page-motion-hook #'dw/present-prepare-slide))

(provide 'dw-present)

dw-irc - IRC Configuration

I prefer rcirc as my default IRC client, it’s simple and gets the job done. Right now I’m using the hosted Soju bouncer on chat.sr.ht but hopefully I’ll move to something self-hosted soon.

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

(use-package rcirc
  :ensure nil
  :custom
  (rcirc-default-nick "daviwil")
  (rcirc-default-user-name "daviwil")
  (rcirc-default-full-name "David Wilson")
  (rcirc-server-alist `(("chat.sr.ht"
                         :port 6697
                         :encryption tls
                         :user-name "daviwil/irc.libera.chat@emacs")))

  (rcirc-reconnect-delay 5)
  (rcirc-fill-column 90)
  (rcirc-track-ignore-server-buffer-flag t)

  :config
  ;; Annoy me, please
  (rcirc-track-minor-mode 1)

  ;; See: https://idiomdrottning.org/rcirc-soju
  (defun-rcirc-command detach (channel)
    "Detach channel to soju."
    (interactive "sPart channel: ")
    (let ((channel (if (> (length channel) 0) channel target)))
      (rcirc-send-privmsg
       process "BouncerServ"
       (format
        "channel update %s -detached true -reattach-on highlight" channel)))))

(provide 'dw-irc)

dw-0x0 - Share Text and Files with 0x0.st

The 0x0.el package is really useful and I used it for a long time. However, it’s currently only on MELPA and I really only use a couple of the functions it provides.

Here’s my own minimal implementation from scratch, please feel free to copy into your own config!

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

(defun dw/0x0-upload-text ()
  (interactive)
  (let* ((contents (if (use-region-p)
		       (buffer-substring-no-properties (region-beginning) (region-end))
		     (buffer-string)))
	 (temp-file (make-temp-file "0x0" nil ".txt" contents)))
    (message "Sending %s to 0x0.st..." temp-file)
    (let ((url (string-trim-right
		(shell-command-to-string
		 (format "curl -s -F'file=@%s' https://0x0.st" temp-file)))))
      (message "The URL is %s" url)
      (kill-new url)
      (delete-file temp-file))))

(defun dw/0x0-upload-file (file-path)
  (interactive "fSelect a file to upload: ")
  (message "Sending %s to 0x0.st..." file-path)
  (let ((url (string-trim-right
	      (shell-command-to-string
	       (format "curl -s -F'file=@%s' https://0x0.st" (expand-file-name file-path))))))
    (message "The URL is %s" url)
    (kill-new url)))

(provide 'dw-0x0)

dw-telegram - telega.el configuration

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

(use-package tracking
  :demand t)

(use-package telega
  :commands telega
  :config
  (setq telega-use-tracking-for '(or unmuted mention)
        telega-completing-read-function #'completing-read
        telega-msg-rainbow-title t
        telega-chat-fill-column 75)

  ;; Show notifications in the mode line
  (add-hook 'telega-load-hook #'telega-mode-line-hook)

  ;; Disable chat buffer auto-fill
  (add-hook 'telega-chat-mode-hook #'telega-chat-auto-fill-mode)

  (when (eq dw/current-distro 'void)
    (setq telega-server-libs-prefix "/usr")))

(provide 'dw-telegram)

dw-desktop - Desktop Environment Utilities

When used as part of a desktop environment, here are some packages that will make it easier for Emacs to drive most of the experience.

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

;; Integrate with the system clipboard
(unless (display-graphic-p)
  (use-package xclip
    :demand t
    :config
    (xclip-mode 1)))

(use-package bluetooth)

;; Control NetworkManager via nmcli
(use-package nm
  :vc (:url "https://github.com/Kodkollektivet/emacs-nm"
       :rev :newest))

(provide 'dw-desktop)

dw-auth - Authentication Utilities

I use GPG primarily when authenticating with Git remotes or opening pass entries. pinentry is the tool that enables me to prompt for my GPG passphrase inside of Emacs.

auth-source-pass enables the use of pass entries for automatically loading credentials for certain things like my IRC bouncer password.

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

;; Use `pass` as an auth-source
(when (file-exists-p "~/.password-store")
  (auth-source-pass-enable))

;; Enable GPG passphrase entry
(use-package pinentry
  :demand t
  :config
  (pinentry-start))

(provide 'dw-auth)

Machine-Specific Settings

This section contains settings that are specific to various systems, particularly to choose extension modules to be loaded on top of the base configuration.

zerocool

(setq dw/use-config-modules
      (append dw/common-config-modules
              '(dw-desktop
                dw-telegram)))

phantom

(setq dw/use-config-modules
      (append dw/common-config-modules
              '(dw-desktop)))

daviwil-x1e

(setq dw/use-config-modules dw/common-config-modules)
(setq xclip-method 'powershell)