Literate Dotfile Configuration

Welcome to Anton's .dotfiles

Welcome to this org document.
It's mixto writing and code… just some symbols…

It is written in a style known as literate style [Donald Knuth].

This literate .dotfile Org document is
the "single source of truth" for configuratin' me macheens.
Every time I make a change… I give it an explanation here first.
After that, then… the code is tangled.

If yer readin' in a web browser, you'll see only de code.
You're missing the magic links of org.

Each code block you will see from Org mode is manually linked to a specific file.
(a.k.a. tangled -> i.e. automatically inserted into a file)
U gotta read it in emacs to see the magic of org.
One of these lifetimes, emacs will find you.

The tangle of code within these writings… it feels
…as Donald said: [I'm paraphrasing]

your code should tell a story

Workflow: How To Use It

Literate Programming

Accepting the practice of writing a literate config means accepting a more complicated workflow.

Since we are authoring a story intertwined with code… as equals…
we have to deliberately place the codes…

This configuration uses literate programming.1

Chezmoi manages the deployed files.2

This applies to the source document.3
It also applies to generated configuration.3

Tangling

Instead of editing our dotfiles directly, we must:
  1. update this document
  2. run a command (tangle) to export that code to the right place

Chezmoi

I've been using chezmoi…
so I already have a similar extra step in the form of chezmoi apply.

Combine Them: Tangle and Apply

However, we'll write a script to help us with these two extra steps.

Run the helper !!M-x tangle-moi!! to:
  1. save and tangle, then
  2. chezmoi apply

Config

Below are different sections of the config.

Physical

Direct Input: hands & keyboard

Before installin' n-e-ting… check ya hands an' keys.

Hands

  • touch type: 1.0 accuracy over speed
  • use backspace? walk away and do push/pull-up

Keys

Kinesis Advantage 360
  • no cap labels: let the keys come to you
  • home-row-mods
  • colemak-dh

Customization of the Kinesis Advantage 360 is in a separate repo (opens in a new tab).

Built-in Mac Keeb

Handled below due to sequential nature of tangling.

Alternative Inputs: voice, face, eyes, body

voice

Talon
os: mac
-
hello talon: "hello world"
select all: key(cmd-a)
copy that: key(cmd-c)
paste that: key(cmd-v)
undo that: key(cmd-z)
redo that: key(cmd-shift-z)

Digital

Chezmoi

First, get chezmoi!

First, We need to:
  1. clone dotfiles repo
  2. run install.sh

Usage:

git clone github.com/antoaenono/dotfiles.git ~/.local/share/chezmoi
cd ~/.local/share/chezmoi
./install.sh

install.sh script:

set -e
# Install Homebrew (if not already installed)
if ! command -v brew &>/dev/null; then
    # Download and run the official Homebrew installer
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    # Add brew to PATH for the current session (Apple Silicon path)
    eval "$(/opt/homebrew/bin/brew shellenv)"
fi
# Install chezmoi (dotfile manager)
brew install chezmoi
# Apply dotfiles, run install scripts
chezmoi apply

echo ""
echo "Done. Restart your computer to ensure everything is loaded correctly."
Notes on the Homebrew install:

Configuration

[scriptEnv]
    VAR_IN_SCRIPT = "a_value"

Repository-only files are excluded from the target home directory.

**/README.*
config.org
install.sh
Brewfile
AGENTS.md
.codex/**

# Skip macOS-only scripts on non-macOS systems
{{ if ne .chezmoi.os "darwin" }}
.chezmoiscripts/darwin/**
{{ end }}

macOS

Homebrew Packages via Brewfile

This script runs brew bundle whenever the Brewfile content changes.4

set -e
# Brewfile hash: {{ include "Brewfile" | sha256sum }}
brew bundle --no-upgrade --file="{{ joinPath .chezmoi.sourceDir "Brewfile" }}"

Shell (Zsh)

oh-my-zsh
set -e
if [ ! -d "$HOME/.oh-my-zsh" ]; then
    git clone --depth=1 https://github.com/ohmyzsh/ohmyzsh.git "$HOME/.oh-my-zsh"
fi
Starship
brew "starship"

Starship init is handled by the oh-my-zsh starship plugin (see plugins in .zshrc).

Aliases
alias h='history'
alias l='ls -la'
Functions
# Create and then navigate to new dir
mkd() {
  mkdir -p "$1" && cd "$1"
}
Zsh Configuration Files

These are documented in order they are loaded.

zshenv

Every shell (login, interactive, script). Loaded first, always.
The ~/.zshenv is a symlink to ~/.config/zsh/.zshenv, which bootstraps ZDOTDIR so zsh finds the rest of its config in ~/.config/zsh/.
In other words, because we're using the XDG_CONFIG_HOME standard (~/.config directory), we need to symlink .zshenv which MUST be in the home directory.

Symlink:

{{ .chezmoi.homeDir }}/.config/zsh/.zshenv
export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:=${HOME}/.config}
export ZDOTDIR=${ZDOTDIR:=${XDG_CONFIG_HOME}/zsh}
zprofile

Login shells only, before .zshrc. Runs once at login, not every new tab.

# Set PATH for Homebrew
eval "$(/opt/homebrew/bin/brew shellenv)"
zshrc

Interactive shells only. Every new terminal tab.
This is where the bulk of shell config lives.

# oh-my-zsh
export ZSH="$ZDOTDIR/ohmyzsh"
zstyle ':omz:update' mode reminder
plugins=(git starship)
source $ZSH/oh-my-zsh.sh

# Aliases
source ~/.config/zsh/alias.zsh

# Functions
source ~/.config/zsh/function.zsh
zlogin

Login shells only, after .zshrc. Not currently used.

zlogout

On shell exit. Not currently used.

Core CLI tools

brew "coreutils"
brew "fd"
brew "ripgrep"
brew "wget"

AeroSpace

Tiling window manager

tap "nikitabobko/tap", trusted: { cask: "aerospace" }
cask "nikitabobko/tap/aerospace"
Commands

All commands: https://nikitabobko.github.io/AeroSpace/commands (opens in a new tab)

Run commands from:
  1. terminal (prefix with aerospace)
  2. key binding

If shell completion is working, try aerospace TAB to see all commands.

Example: open terminal with alt + enter shortcut (like in i3)
https://nikitabobko.github.io/AeroSpace/commands#exec-and-forget (opens in a new tab)

alt-enter = '''exec-and-forget osascript -e '
tell application "Terminal"
    do script
    activate
end tell'
'''
Startup
start-at-login = true

# https://github.com/FelixKratz/JankyBorders
after-startup-command = [
  'exec-and-forget borders active_color=0xff7dcfff inactive_color=0xff494d64 width=10.0'
]
set -e
/usr/bin/defaults write bobko.aerospace displayStyle -string squares
Properties
# Focus: mouse follow
#on-focused-monitor-changed = ['move-mouse monitor-lazy-center'] # follow monitor
on-focus-changed = ['move-mouse window-lazy-center'] # follow window
# https://nikitabobko.github.io/AeroSpace/commands#move-mouse
# https://nikitabobko.github.io/AeroSpace/guide#on-focus-changed-callbacks

# Window: layouts
# https://nikitabobko.github.io/AeroSpace/guide#layouts
default-root-container-layout = 'tiles' # tiles|accordion
default-root-container-orientation = 'auto' # horizontal|vertical|auto
# auto: wide monitor => horizontal, tall monitor => vertical
accordion-padding = 42

# Window: tree structure for containers: normalization property
# https://nikitabobko.github.io/AeroSpace/guide#normalization
enable-normalization-flatten-containers = true
enable-normalization-opposite-orientation-for-nested-containers = true

# Misc:
# Toggle off macOS "Hide application", useful if accidentally hit cmd-h
# https://nikitabobko.github.io/AeroSpace/goodies#disable-hide-app
automatically-unhide-macos-hidden-apps = true
Key-mapping
  • don't think I need this any more…
  • not sure what it's for since we override bindings anyway
# https://nikitabobko.github.io/AeroSpace/guide#key-mapping
# [key-mapping]
#     preset = 'qwerty' # qwerty|dvorak|colemak

Gaps

# Gaps between windows (inner-*) and between monitor edges (outer-*).
# Possible values:
# - Constant:     gaps.outer.top = 8
# - Per monitor:  gaps.outer.top = [{ monitor.main = 16 }, { monitor."some-pattern" = 32 }, 24]
#                 In this example, 24 is a default value when there is no match.
#                 Monitor pattern is the same as for 'workspace-to-monitor-force-assignment'.
#                 See:
#                 https://nikitabobko.github.io/AeroSpace/guide#assign-workspaces-to-monitors
[gaps]
    inner.horizontal = 2
    inner.vertical =   0

    outer.left =       5
    outer.bottom =     5
    outer.top =        5
    outer.right =      5
Key Bindings
Key Names
  • Letters. a, b, c, …, z
  • Numbers. 0, 1, 2, …, 9
  • Keypad numbers. keypad0, keypad1, keypad2, …, keypad9
  • F-keys. f1, f2, …, f20
  • Special keys. minus, equal, period, comma, slash, backslash, quote, semicolon,
    backtick, leftSquareBracket, rightSquareBracket, space, enter, esc,
    backspace, tab, pageUp, pageDown, home, end, forwardDelete,
    sectionSign (ISO keyboards only, european keyboards only)
  • Keypad special. keypadClear, keypadDecimalMark, keypadDivide, keypadEnter, keypadEqual,
    keypadMinus, keypadMultiply, keypadPlus
  • Arrows. left, down, up, right
  • Modifiers. cmd, alt, ctrl, shift
Main Mode
[mode.main.binding]
    # Modes  https://nikitabobko.github.io/AeroSpace/commands#mode
    # You can make up any kind of mode you want!
    # https://nikitabobko.github.io/AeroSpace/guide#binding-modes
    alt-cmd-semicolon = 'mode service'

    # Layout https://nikitabobko.github.io/AeroSpace/commands#layout
    alt-cmd-period= 'layout accordion vertical horizontal'
    alt-cmd-slash = 'layout tiles horizontal vertical'

    # Resize https://nikitabobko.github.io/AeroSpace/commands#resize
    alt-cmd-minus = 'resize smart -150'
    alt-cmd-equal = 'resize smart +150'

    # Focus https://nikitabobko.github.io/AeroSpace/commands#focus
    alt-cmd-h = 'focus left'
    alt-cmd-left = 'focus left'
    alt-cmd-j = 'focus down'
    alt-cmd-down = 'focus down'
    alt-cmd-k = 'focus up'
    alt-cmd-up= 'focus up'
    alt-cmd-l = 'focus right'
    alt-cmd-right = 'focus right'

    # Move https://nikitabobko.github.io/AeroSpace/commands#move
    ctrl-alt-cmd-h = 'move left'
    ctrl-alt-cmd-left = 'move left'
    ctrl-alt-cmd-j = 'move down'
    ctrl-alt-cmd-down = 'move down'
    ctrl-alt-cmd-k = 'move up'
    ctrl-alt-cmd-up = 'move up'
    ctrl-alt-cmd-l = 'move right'
    ctrl-alt-cmd-right = 'move right'

    # Workspace recent https://nikitabobko.github.io/AeroSpace/commands#workspace-back-and-forth
    alt-cmd-tab = 'workspace-back-and-forth'
    # Workspace monitor https://nikitabobko.github.io/AeroSpace/commands#move-workspace-to-monitor
    ctrl-alt-cmd-tab = 'move-workspace-to-monitor --wrap-around next'

    # Workspace show https://nikitabobko.github.io/AeroSpace/commands#workspace
    alt-cmd-1 = 'workspace 1'
    alt-cmd-2 = 'workspace 2'
    alt-cmd-3 = 'workspace 3'
    alt-cmd-4 = 'workspace 4'
    alt-cmd-5 = 'workspace 5'
    alt-cmd-6 = 'workspace 6'
    alt-cmd-7 = 'workspace 7'
    alt-cmd-8 = 'workspace 8'
    alt-cmd-9 = 'workspace 9'
    alt-cmd-a = 'workspace A'
    alt-cmd-b = 'workspace B'
    alt-cmd-c = 'workspace C'
    alt-cmd-d = 'workspace D'
    alt-cmd-e = 'workspace E'
    alt-cmd-f = 'workspace F'
    alt-cmd-g = 'workspace G'
    alt-cmd-i = 'workspace I'
    alt-cmd-m = 'workspace M'
    alt-cmd-n = 'workspace N'
    alt-cmd-o = 'workspace O'
    alt-cmd-p = 'workspace P'
    alt-cmd-q = 'workspace Q'
    alt-cmd-r = 'workspace R'
    alt-cmd-s = 'workspace S'
    alt-cmd-t = 'workspace T'
    alt-cmd-u = 'workspace U'
    alt-cmd-w = 'workspace W'
    alt-cmd-x = 'workspace X'
    alt-cmd-y = 'workspace Y'
    alt-cmd-z = 'workspace Z'

    # Workspace move and follow https://nikitabobko.github.io/AeroSpace/commands#move-node-to-workspace
    ctrl-alt-cmd-1 = 'move-node-to-workspace --focus-follows-window 1'
    ctrl-alt-cmd-2 = 'move-node-to-workspace --focus-follows-window 2'
    ctrl-alt-cmd-3 = 'move-node-to-workspace --focus-follows-window 3'
    ctrl-alt-cmd-4 = 'move-node-to-workspace --focus-follows-window 4'
    ctrl-alt-cmd-5 = 'move-node-to-workspace --focus-follows-window 5'
    ctrl-alt-cmd-6 = 'move-node-to-workspace --focus-follows-window 6'
    ctrl-alt-cmd-7 = 'move-node-to-workspace --focus-follows-window 7'
    ctrl-alt-cmd-8 = 'move-node-to-workspace --focus-follows-window 8'
    ctrl-alt-cmd-9 = 'move-node-to-workspace --focus-follows-window 9'
    ctrl-alt-cmd-a = 'move-node-to-workspace --focus-follows-window A'
    ctrl-alt-cmd-b = 'move-node-to-workspace --focus-follows-window B'
    ctrl-alt-cmd-c = 'move-node-to-workspace --focus-follows-window C'
    ctrl-alt-cmd-d = 'move-node-to-workspace --focus-follows-window D'
    ctrl-alt-cmd-e = 'move-node-to-workspace --focus-follows-window E'
    ctrl-alt-cmd-f = 'move-node-to-workspace --focus-follows-window F'
    ctrl-alt-cmd-g = 'move-node-to-workspace --focus-follows-window G'
    ctrl-alt-cmd-i = 'move-node-to-workspace --focus-follows-window I'
    ctrl-alt-cmd-m = 'move-node-to-workspace --focus-follows-window M'
    ctrl-alt-cmd-n = 'move-node-to-workspace --focus-follows-window N'
    ctrl-alt-cmd-o = 'move-node-to-workspace --focus-follows-window O'
    ctrl-alt-cmd-p = 'move-node-to-workspace --focus-follows-window P'
    ctrl-alt-cmd-q = 'move-node-to-workspace --focus-follows-window Q'
    ctrl-alt-cmd-r = 'move-node-to-workspace --focus-follows-window R'
    ctrl-alt-cmd-s = 'move-node-to-workspace --focus-follows-window S'
    ctrl-alt-cmd-t = 'move-node-to-workspace --focus-follows-window T'
    ctrl-alt-cmd-u = 'move-node-to-workspace --focus-follows-window U'
    ctrl-alt-cmd-w = 'move-node-to-workspace --focus-follows-window W'
    ctrl-alt-cmd-x = 'move-node-to-workspace --focus-follows-window X'
    ctrl-alt-cmd-y = 'move-node-to-workspace --focus-follows-window Y'
    ctrl-alt-cmd-z = 'move-node-to-workspace --focus-follows-window Z'
Service Mode
[mode.service.binding]
    esc = ['reload-config', 'mode main']

    # Window float toggle
    f = ['layout floating tiling', 'mode main']

    # r = ['flatten-workspace-tree', 'mode main'] # reset layout
    # backspace = ['close-all-windows-but-current', 'mode main']

    # sticky is not yet supported https://github.com/nikitabobko/AeroSpace/issues/2
    # s = ['layout sticky tiling', 'mode main']

    # alt-shift-h = ['join-with left', 'mode main']
    # alt-shift-j = ['join-with down', 'mode main']
    # alt-shift-k = ['join-with up', 'mode main']
    # alt-shift-l = ['join-with right', 'mode main']

    # down = 'volume down'
    # up = 'volume up'
    # shift-down = ['volume set 0', 'mode main']
Automations
# To find an `app-id`, use `aerospace list-apps`
# https://nikitabobko.github.io/AeroSpace/commands#list-apps

# Automatically run functions for specific windows
# https://nikitabobko.github.io/AeroSpace/guide#on-window-detected-callback

# Only `move-node-to-workspace`, `layout tiling`, `layout floating` are supported
# https://github.com/nikitabobko/AeroSpace/issues/20
# this limitation is supposed to be removed in 0.2
# https://github.com/nikitabobko/AeroSpace/milestone/5
[[on-window-detected]]
  if.app-id = 'com.apple.ActivityMonitor'
  run = ['move-node-to-workspace A']

[[on-window-detected]]
  if.app-id = 'org.gnu.Emacs'
  run = ['move-node-to-workspace E']

[[on-window-detected]]
  if.app-id = 'com.google.Chrome'
  run = ['move-node-to-workspace C']

[[on-window-detected]]
  if.app-id = 'com.apple.finder'
  run = ['move-node-to-workspace F']

[[on-window-detected]]
  if.app-id = 'com.apple.Safari'
  run = ['move-node-to-workspace S']

[[on-window-detected]]
  if.app-id = 'com.mitchellh.ghostty'
  run = ['move-node-to-workspace T']

[[on-window-detected]]
  if.app-id = 'com.openai.codex'
  run = ['move-node-to-workspace X']

[[on-window-detected]]
  if.app-id = 'dev.zed.Zed'
  run = ['move-node-to-workspace Z']

BitWarden

SSH agent

To defer to Bitwarden's SSH agent on `commit` and `push`,
this environment variable must be set:

# Bitwarden SSH Agent
export SSH_AUTH_SOCK=$HOME/Library/Containers/com.bitwarden.desktop/Data/.bitwarden-ssh-agent.sock

LaTeX

cask "mactex"

After a major mactex upgrade, TeX Live regenerates format files (opens in a new tab) (fmtutil) and font map files (updmap) on first run. This blocks any process that calls pdflatex synchronously (including Emacs during org export). See TeX Live Guide §3.4.4: System font configuration (opens in a new tab) and updmap/fmtutil sys vs user (opens in a new tab) for background. The script below warms the cache after each mactex upgrade.

# mactex version: {{ output "brew" "list" "--cask" "--versions" "mactex" | trim }}
# Warms TeX Live font and format caches on first run after install or upgrade.
# Without this, the first pdflatex call blocks synchronously while regenerating
# caches, which freezes any process waiting on it (e.g. Emacs during org export).
# Only warms pdflatex; to warm all engines (xelatex, lualatex, etc.) use instead:
#   fmtutil-user --all && updmap-user  (~90s)
set -e
pdflatex -interaction=nonstopmode /dev/null 2>/dev/null || true

Emacs

This standalone Mac application (opens in a new tab) is well maintained.

tap "jimeh/emacs-builds", trusted: { cask: "emacs-app" }
cask "jimeh/emacs-builds/emacs-app"
Starting Emacs

--alternate-editor""= is a historical hack: passing empty string triggers Emacs to start a daemon as the fallback if no server is running.

# ec: open emacsclient frame, detached (-n), starting daemon if needed
alias ec='emacsclient -c -n --alternate-editor=""'
# ecc: interactive emacsclient - reports server status, then opens a frame
ecs() {
  if [[ "$1" == "-k" ]]; then
    if ! emacsclient -e t &>/dev/null; then
      echo "no server running"
      return 1
    fi
    read "reply?kill server? [y/n] "
    if [[ "$reply" == "y" ]]; then
      emacsclient -e '(kill-emacs)'
      echo "server killed"
    else
      echo "aborted"
    fi
    return
  fi
  if emacsclient -e t &>/dev/null; then
    echo "server: running"
  else
    echo "server: not found - starting daemon..."
    emacs --daemon
  fi
  local server_name
  server_name=$(emacsclient -e 'server-name' | tr -d '"')
  echo "--- emacs state ---"
  emacs --batch --eval "(progn (require 'server) (princ (server-eval-at \"$server_name\" '(let* ((frames (seq-filter (lambda (f) (frame-parameter f 'client)) (frame-list))) (lines (seq-map-indexed (lambda (f i) (format \"frame %d:\\n  workspace: %s\\n  windows (active buffer): %s\" (1+ i) (with-selected-frame f (persp-name (get-current-persp))) (string-join (mapcar (lambda (w) (buffer-name (window-buffer w))) (window-list f)) \", \"))) frames))) (string-join lines \"\\n\")))))" 2>/dev/null
  emacsclient -c -n
  echo "\n\nnew frame opened"
}
Doom
Install

Download

set -e
xcode-select -p &>/dev/null || xcode-select --install
if [ ! -d "$HOME/.emacs.d" ]; then
    git clone --depth=1 https://github.com/doomemacs/doomemacs "$HOME/.emacs.d"
    "$HOME/.emacs.d/bin/doom" install
fi

Add doom command to PATH

# doom
export PATH="$HOME/.emacs.d/bin:$PATH"
Configuration
tangle-moi.el

Portable Emacs implementation shared by the interactive command and agent
launcher.

;;; tangle-moi.el --- Tangle the literate dotfiles config -*- lexical-binding: t; -*-

(require 'ob-tangle)
(require 'subr-x)

(defun tangle-moi--source-directory ()
  "Return the chezmoi source directory."
  (file-name-as-directory
   (string-trim (car (process-lines "chezmoi" "source-path")))))

(defun tangle-moi--config-file ()
  "Return the canonical config.org path."
  (file-truename
   (expand-file-name "config.org" (tangle-moi--source-directory))))

(defun tangle-moi-tangle ()
  "Tangle config.org while preserving any unsaved live Emacs buffer."
  (let* ((source-directory (tangle-moi--source-directory))
         (config-file (tangle-moi--config-file))
         (existing-buffer (find-buffer-visiting config-file)))
    (when (and existing-buffer (buffer-modified-p existing-buffer))
      (user-error "Refusing to tangle: config.org has unsaved Emacs changes"))
    (with-current-buffer (or existing-buffer (find-file-noselect config-file))
      (unless (verify-visited-file-modtime (current-buffer))
        (revert-buffer t t))
      (let ((default-directory source-directory))
        (save-restriction
          (widen)
          (org-babel-tangle))))))

(defun tangle-moi-tangle-batch ()
  "Tangle config.org and print the target files for batch callers."
  (condition-case error-data
      (progn
        (prin1 (tangle-moi-tangle))
        (terpri))
    (error
     (message "Tangle failed: %s" (error-message-string error-data))
     (kill-emacs 1))))

(defun tangle-moi--save-config-buffer ()
  "Save config.org when it has unsaved changes in Emacs."
  (when-let ((buffer (find-buffer-visiting (tangle-moi--config-file))))
    (when (buffer-modified-p buffer)
      (with-current-buffer buffer
        (save-buffer)))))

;;;###autoload
(defun tangle-moi ()
  "Tangle the literate dotfiles config, preview it, and ask to apply."
  (interactive)
  (tangle-moi--save-config-buffer)
  (message "Tangling Org file...")
  (tangle-moi-tangle)
  (message "Tangling complete. Previewing chezmoi changes...")
  (let ((diff (shell-command-to-string
               "chezmoi apply --dry-run --verbose --force")))
    (if (string-empty-p diff)
        (message "No changes to apply.")
      (with-output-to-temp-buffer "*chezmoi diff*"
        (princ diff))
      (if (y-or-n-p
           "Apply these changes, overwriting any drifted targets? ")
          (if (zerop
               (shell-command "chezmoi apply --verbose --force"))
              (message "Changes applied.")
            (message "Chezmoi apply failed; see *Shell Command Output*."))
        (message "Changes aborted.")))))

(provide 'tangle-moi)
;;; tangle-moi.el ends here
config.el
;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-

; Remember, you do not need to run 'doom sync' after modifying this file!

;; Some functionality uses this to identify you, e.g. GPG configuration, email
;; clients, file templates and snippets. It is optional.
;; (setq user-full-name "John Doe"
;;       user-mail-address "[email protected]")

;; Doom exposes five (optional) variables for controlling fonts in Doom:
;;
;; - `doom-font' -- the primary font to use
;; - `doom-variable-pitch-font' -- a non-monospace font (where applicable)
;; - `doom-big-font' -- used for `doom-big-font-mode'; use this for
;;   presentations or streaming.
;; - `doom-symbol-font' -- for symbols
;; - `doom-serif-font' -- for the `fixed-pitch-serif' face
;;
;; See 'C-h v doom-font' for documentation and more examples of what they
;; accept. For example:
;;
;;(setq doom-font (font-spec :family "Fira Code" :size 12 :weight 'semi-light)
;;      doom-variable-pitch-font (font-spec :family "Fira Sans" :size 13))
;;
;; If you or Emacs can't find your font, use 'M-x describe-font' to look them
;; up, `M-x eval-region' to execute elisp code, and 'M-x doom/reload-font' to
;; refresh your font settings. If Emacs still can't find your font, it likely
;; wasn't installed correctly. Font issues are rarely Doom issues!

(setopt
  user-full-name "Anton Bilbaeno"
  user-mail-address "[email protected]"
  display-line-numbers-type 'relative
  mouse-drag-copy-region t
)

(setq
  doom-theme 'doom-monokai-machine
)

(setq
  projectile-project-search-path '("~/source/" "~/.local/share/chezmoi/")
  projectile-globally-ignored-directories '("node_modules" ".cache" "deps" "_build" "dist" ".elixir_ls" ".hex" ".mix")
  projectile-ignored-projects '("~/source/archive/")
)

(load! "lisp/tangle-moi")


;; 80 char limit encourages concise code. Respect to the early days.
;; note: in org files 80 chars is /on top/ of the spacing under the heading
(setq-default fill-column 80)
(global-display-fill-column-indicator-mode 1)

;; Rebind "quit" to Cmd-Shift-q to avoid conflict with M-q (fill-paragraph)
;; ...shouldn't be so easy to quit anyway!
(map! :g "M-Q" #'save-buffers-kill-terminal)
;; TODO the above doesn't work, it just adds a new binding, and Emacs Gui still captures Cmd+q


;; HASKELL
;; Hooks so haskell and literate haskell major modes trigger LSP setup
(add-hook 'haskell-mode-hook #'lsp)
(add-hook 'haskell-literate-mode-hook #'lsp)

;; TODO test if these are really needed after moving path to .zshenv (was in .zshrc)
;; Add haskell lsp to path for emacs subprocesses
(add-to-list 'exec-path (expand-file-name "~/.ghcup/bin"))
;; Add haskell tools to path for emacs environment
(setenv "PATH" (concat (expand-file-name "~/.ghcup/bin") ":" (getenv "PATH")))
;; hlint in local bin. Ensure ~/.local/bin is in Emacs's exec-path
;; TODO this didnt work emacs still can't find hlint
(setq exec-path (cons (expand-file-name "~/.local/bin") exec-path))

;; ORG ;;
(setopt org-directory "~/org/")

;; Restore Org's native folded -> children -> subtree TAB cycle. Doom's
;; evil-org integration otherwise replaces it with a two-state local toggle.
(after! evil-org
  (remove-hook 'org-tab-first-hook #'+org-cycle-only-current-subtree-h))

;; Disable org-element cache (Org 9.5+ bug): subtree and narrowed exports trigger
;; cache revalidation on the temp export buffer, causing an infinite loop / hang.
;; Full-document export is unaffected. Disabling the cache trades minor parse speed
;; for reliable export at any scope.
(setq org-element-use-cache nil)

;; (after! org
;;   ;; Add the 'titletoc' package to Org's list of LaTeX packages
;;   (add-to-list 'org-latex-packages-alist '("titletoc" nil t))

;;   ;; Use latexmk for PDF export
;;   ;; (setq org-latex-to-pdf-process (list "latexmk %f"))
;; )

;; more `<` tab completion templates aka "structure templates" -- orgmode.org/manual/Structure-Templates.html
;(after! org (add-to-list 'org-structure-template-alist
  ;("sh" . "src shell")))

;; custom syntax: !! to indicate key presses
;; add as a keyword in org-mode and draw a box around it
;; TODO export to <kbd>
(after! org
  (font-lock-add-keywords
   'org-mode
   '(("!!\\(.*?\\)!!"
      (1 '(face (:weight 'semi-bold
                           :box '(:line-width 2 :color "palegoldenrod"))))))))

;; automatically show magic behind point
(use-package! org-appear
  :after org
  :hook (org-mode . org-appear-mode)
  :config
  (setopt org-appear-autoemphasis t ; _/**/_
          org-appear-autolinks t ;; dun work on #+TRANSCLUSION directives. use `org-toggle-link-display`
          org-appear-autoentities t ;; \alpha for α
  )
)

; information teleport
(use-package! org-transclusion
  :after org
  :init
  (map!
   :map global-map "<f12>" #'org-transclusion-add
   :leader
   :prefix "n"
   :desc "Org Transclusion Mode" "t" #'org-transclusion-mode))

; https://nobiot.github.io/org-transclusion/#:~:text=4.10%20Extensions,regain%20the%20indentation.
; this didn't seem to work, or i don't understand what it's supposed to be doing
; but i was trying to fix the issue where the source buffer loses indentation while transclusion is active
  ; :config
  ; ;; Enable org-transclusion-indent-mode extension
  ; (add-to-list 'org-transclusion-extensions 'org-transclusion-indent-mode)
  ; (require 'org-transclusion-indent-mode))

; roaman emacs... to gather
(setopt org-roam-directory "~/org/roam")
(org-roam-db-autosync-mode) ; docs say put this here[?]

;; CITATIONS (citar + org-cite)
(setq! citar-bibliography '("~/org/bib/references.bib")
       citar-library-paths '("~/org/pdfs/")
       citar-notes-paths '("~/org/roam/refs/"))

(after! oc
  (setq org-cite-global-bibliography '("~/org/bib/references.bib")
        org-cite-insert-processor 'citar
        org-cite-follow-processor 'citar
        org-cite-activate-processor 'citar))

;; bridge citar with org-roam for note management
(use-package! citar-org-roam
  :after (citar org-roam)
  :config
  (citar-org-roam-mode)
  (setq citar-org-roam-subdir "refs"
        citar-org-roam-note-title-template "${author}, ${title}"
        citar-org-roam-template-fields
        '((:citar-title   . ("title"))
          (:citar-author  . ("author" "editor"))
          (:citar-date    . ("date" "year" "issued"))
          (:citar-pages   . ("pages"))
          (:citar-type    . ("=type="))
          (:citar-file    . ("file"))))
  (add-to-list 'org-roam-capture-templates
               '("r" "reference" plain "%?"
                 :if-new (file+head "refs/${citar-citekey}.org"
                                    ":PROPERTIES:\n:NOTER_DOCUMENT: ${citar-file}\n:END:\n#+title: ${citar-title}\n#+DATE: %<%Y-%m-%d %a>\n#+PUBLISH: garden\n")
                 :immediate-finish t
                 :unnarrowed t))
  (setq citar-org-roam-capture-template-key "r"))

(defun my/citar-open-noter ()
  "Open roam note and start org-noter in a dedicated frame."
  (interactive)
  (select-frame (make-frame))
  (call-interactively #'citar-open-notes)
  (org-noter))

(map! :leader :desc "Open noter session" "n p" #'my/citar-open-noter)

(defun my/citar-close-noter ()
  "Save notes and PDF annotations, end org-noter session, and close frame."
  (interactive)
  (org-noter--with-valid-session
    (with-current-buffer (org-noter--session-notes-buffer session)
      (save-buffer))
    (with-current-buffer (org-noter--session-doc-buffer session)
      (save-buffer)))
  (org-noter-kill-session)
  (delete-frame))

(map! :leader :desc "Close noter session" "n q" #'my/citar-close-noter)

;; PDF ANNOTATION (org-noter)
(use-package! org-noter
  :after org
  :config
  (setq org-noter-always-create-frame nil
        org-noter-kill-frame-at-session-end nil
        org-noter-auto-save-last-location nil
        org-noter-doc-property-in-notes t
        org-noter-notes-window-location 'horizontal-split
        org-noter-highlight-selected-text t))

;; GLOSSARY / ACRONYMS
(setq org-link-descriptive t
      org-glossary-global-terms '("~/org/glossary.org"))
(use-package! org-glossary
  :after org
  :hook (org-mode . org-glossary-mode))

;; ORG HEADING FACES (levels 5-8 inherit outline-4/foreground by default)
(custom-set-faces!
  '(org-level-5 :foreground "#f92672")  ; pink
  '(org-level-6 :foreground "#fd971f")  ; orange
  '(org-level-7 :foreground "#ae81ff")  ; purple
  '(org-level-8 :foreground "#66d9ef")) ; teal

;; ROAM UI (graph visualization)
(use-package! org-roam-ui
  :after org-roam
  :config
  (setq org-roam-ui-sync-theme t
        org-roam-ui-follow t
        org-roam-ui-update-on-save t))

;; CAPTURE TEMPLATES
(after! org
  (setq org-capture-templates
        '(("t" "Task" entry (file+headline "~/org/inbox.org" "Tasks")
           "* TODO %?\n%U\n%a\n")
          ("n" "Note" entry (file+headline "~/org/inbox.org" "Notes")
           "* %?\n%U\n")
          ("r" "Reading Note" entry (file+headline "~/org/inbox.org" "Reading")
           "* %^{Title}\n%U\n%?\n"))))

;; REFILE
(after! org
  (setq org-refile-targets '((nil :maxlevel . 3)
                             (org-agenda-files :maxlevel . 3))
        org-refile-use-outline-path 'file
        org-outline-path-complete-in-steps nil
        org-refile-allow-creating-parent-nodes 'confirm))

;; AGENDA (recursive - finds TODOs in all subdirs)
(setq org-agenda-files '("~/org/")
      org-agenda-file-regexp "\\`[^.].*\\.org\\'")
(setq org-agenda-custom-commands
      '(("d" "Dashboard"
         ((agenda "" ((org-agenda-span 14)))
          (todo "TODO" ((org-agenda-overriding-header "Tasks")))))))

(defun my/org-agenda-files-recursive ()
  "Build agenda file list recursively from ~/org/."
  (directory-files-recursively "~/org/" "\\.org$"))
(setq org-agenda-files (my/org-agenda-files-recursive))

(defun my/insert-site-file-header (publish-type)
  "Insert a dated draft site header routed to PUBLISH-TYPE."
  (let ((title
         (capitalize
          (string-replace
           "-" " "
           (file-name-base buffer-file-name)))))
    (insert
     (format "#+title: %s\n#+DATE: %s\n#+PUBLISH: %s\n#+DRAFT: true\n\n"
             title
             (format-time-string "<%Y-%m-%d %a>")
             publish-type))))

;; Publishing is metadata-driven, so source location and site destination can
;; vary independently. These rules provide the common directory defaults.
(set-file-template!
 (concat "\\`"
         (regexp-quote (expand-file-name "~/org/blog/"))
         "[^/]+\\.org\\'")
 :trigger (lambda () (my/insert-site-file-header "blog")))

(set-file-template!
 (concat "\\`"
         (regexp-quote (expand-file-name "~/org/now/"))
         "[^/]+\\.org\\'")
 :trigger (lambda () (my/insert-site-file-header "now")))

(set-file-template!
 (concat "\\`"
         (regexp-quote (expand-file-name "~/org/time/"))
         ".+\\.org\\'")
 :trigger (lambda () (my/insert-site-file-header "time")))

;; Whenever you reconfigure a package, make sure to wrap your config in an
;; `after!' block, otherwise Doom's defaults may override your settings. E.g.
;;
;;   (after! PACKAGE
;;     (setq x y))
;;
;; The exceptions to this rule:
;;
;;   - Setting file/directory variables (like `org-directory')
;;   - Setting variables which explicitly tell you to set them before their
;;     package is loaded (see 'C-h v VARIABLE' to look up their documentation).
;;   - Setting doom variables (which start with 'doom-' or '+').
;;
;; Here are some additional functions/macros that will help you configure Doom.
;;
;; - `load!' for loading external *.el files relative to this one
;; - `use-package!' for configuring packages
;; - `after!' for running code after a package has loaded
;; - `add-load-path!' for adding directories to the `load-path', relative to
;;   this file. Emacs searches the `load-path' when you load packages with
;;   `require' or `use-package'.
;; - `map!' for binding new keys
;;
;; To get information about any of these functions/macros, move the cursor over
;; the highlighted symbol at press 'K' (non-evil users must press 'C-c c k').
;; This will open documentation for it, including demos of how they are used.
;; Alternatively, use `C-h o' to look up a symbol (functions, variables, faces,
;; etc).
;;
;; You can also try 'gd' (or 'C-c c d') to jump to their definition and see how
;; they are implemented.

;;; magit
(after! magit
  (setq magit-diff-refine-hunk 'all
        magit-diff-highlight-trailing nil
        magit-diff-paint-whitespace nil))

(custom-set-faces!
  '(magit-diff-added             :background unspecified)
  '(magit-diff-added-highlight   :background unspecified)
  '(magit-diff-removed           :background unspecified)
  '(magit-diff-removed-highlight :background unspecified))

;;; difftastic - AST-aware diffs, called on demand from magit
(use-package! difftastic
  :defer t
  :init
  (map! :leader
        :prefix "g"
        :desc "Difftastic show commit"   "D" #'difftastic-magit-show
        :desc "Difftastic diff range"    "T" #'difftastic-magit-diff)
  ;; Note: difftastic lands in doom's bottom popup (small). Press `C-w _` to
  ;; maximize the window vertically, or `C-w o` to fill the frame.
  )

Brewfile entry for the difft binary used by difftastic.el.

brew "difftastic"
init.el
;;; init.el -*- lexical-binding: t; -*-

;; This file controls what Doom modules are enabled and what order they load
;; in. Remember to run 'doom sync' after modifying it!

;; NOTE Press 'SPC h d h' (or 'C-h d h' for non-vim users) to access Doom's
;;      documentation. There you'll find a link to Doom's Module Index where all
;;      of our modules are listed, including what flags they support.

;; NOTE Move your cursor over a module's name (or its flags) and press 'K' (or
;;      'C-c c k' for non-vim users) to view its documentation. This works on
;;      flags as well (those symbols that start with a plus).
;;
;;      Alternatively, press 'gd' (or 'C-c c d') on a module to browse its
;;      directory (for easy access to its source code).

(doom! :input
       ;;(colemak +mod-dh)
       ;;bidi              ; (tfel ot) thgir etirw uoy gnipleh
       ;;chinese
       ;;japanese
       ;;layout            ; auie,ctsrnm is the superior home row

       :completion
       ;;company           ; the ultimate code completion backend
       ;;(corfu +orderless)  ; complete with cap(f), cape and a flying feather!
       ;;helm              ; the *other* search engine for love and life
       ;;ido               ; the other *other* search engine...
       ;;ivy               ; a search engine for love and life
       vertico           ; the search engine of the future

       :ui
       ;;deft              ; notational velocity for Emacs
       doom              ; what makes DOOM look the way it does
       doom-dashboard    ; a nifty splash screen for Emacs
       doom-quit         ; DOOM quit-message prompts when you quit Emacs
       ;;(emoji +unicode)  ; 🙂
       hl-todo           ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW
       indent-guides     ; highlighted indent columns
       ligatures         ; ligatures and symbols to make your code pretty again
       ;;minimap           ; show a map of the code on the side
       modeline          ; snazzy, Atom-inspired modeline, plus API
       nav-flash         ; blink cursor line after big motions
       ;;neotree           ; a project drawer, like NERDTree for vim
       ophints           ; highlight the region an operation acts on
       (popup +all +defaults)   ; tame sudden yet inevitable temporary windows
       ;;smooth-scroll     ; So smooth you won't believe it's not butter
       ;;tabs              ; a tab bar for Emacs
       treemacs          ; a project drawer, like neotree but cooler
       ;;unicode           ; extended unicode support for various languages
       (vc-gutter +pretty) ; vcs diff in the fringe
       vi-tilde-fringe   ; fringe tildes to mark beyond EOB
       window-select     ; visually switch windows
       workspaces        ; tab emulation, persistence & separate workspaces
       ;;zen               ; distraction-free coding or writing

       :editor
       (evil +everywhere); come to the dark side, we have cookies
       file-templates    ; auto-snippets for empty files
       fold              ; (nigh) universal code folding
       ;;(format +onsave)  ; automated prettiness
       ;;god               ; run Emacs commands without modifier keys
       ;;lispy             ; vim for lisp, for people who don't like vim
       ;;multiple-cursors  ; editing in many places at once
       ;;objed             ; text object editing for the innocent
       ;;parinfer          ; turn lisp into python, sort of
       ;;rotate-text       ; cycle region at point between text candidates
       snippets          ; my elves. They type so I don't have to
       ;;word-wrap         ; soft wrapping with language-aware indent

       :emacs
       dired             ; making dired pretty [functional]
       electric          ; smarter, keyword-based electric-indent
       ;;eww               ; the internet is gross
       ;;ibuffer           ; interactive buffer management
       undo              ; persistent, smarter undo for your inevitable mistakes
       vc                ; version-control and Emacs, sitting in a tree

       :term
       eshell            ; the elisp shell that works everywhere
       ;;shell             ; simple shell REPL for Emacs
       ;;term              ; basic terminal emulator for Emacs
       vterm             ; the best terminal emulation in Emacs

       :checkers
       syntax              ; tasing you for every semicolon you forget
       ;;(spell +flyspell) ; tasing you for misspelling mispelling
       ;;grammar           ; tasing grammar mistake every you make

       :tools
       ;;ansible
       biblio            ; Writes a PhD for you (citation needed)
       ;;collab            ; buffers with friends
       ;;debugger          ; FIXME stepping through code, to help you add bugs
       ;;direnv
       ;;docker
       ;;editorconfig      ; let someone else argue about tabs vs spaces
       ;;ein               ; tame Jupyter notebooks with emacs
       (eval +overlay)     ; run code, run (also, repls)
       lookup              ; navigate your code and its documentation
       ;;llm               ; when I said you needed friends, I didn't mean...
       lsp               ; M-x vscode
       magit             ; a git porcelain for Emacs
       make              ; run make tasks from Emacs
       ;;pass              ; password manager for nerds
       (pdf +noter)      ; pdf enhancements with org-noter annotations
       ;;terraform         ; infrastructure as code
       tmux              ; an API for interacting with tmux
       tree-sitter       ; syntax and parsing, sitting in a tree...
       ;;upload            ; map local to remote projects via ssh/ftp

       :os
       (:if (featurep :system 'macos) macos)  ; improve compatibility with macOS
       ;;tty               ; improve the terminal Emacs experience

       :lang
       ;;ada               ; In strong typing we (blindly) trust
       ;;agda              ; types of types of types of types...
       ;;beancount         ; mind the GAAP
       (cc +lsp)         ; C > C++ == 1
       ;;clojure           ; java with a lisp
       ;;common-lisp       ; if you've seen one lisp, you've seen them all
       ;;coq               ; proofs-as-programs
       ;;crystal           ; ruby at the speed of c
       ;;csharp            ; unity, .NET, and mono shenanigans
       data              ; config/data formats
       ;;(dart +flutter)   ; paint ui and not much else
       ;;dhall
       elixir            ; erlang done right
       ;;elm               ; care for a cup of TEA?
       emacs-lisp        ; drown in parentheses
       ;;erlang            ; an elegant language for a more civilized age
       ;;ess               ; emacs speaks statistics
       ;;factor
       ;;faust             ; dsp, but you get to keep your soul
       ;;fortran           ; in FORTRAN, GOD is REAL (unless declared INTEGER)
       ;;fsharp            ; ML stands for Microsoft's Language
       ;;fstar             ; (dependent) types and (monadic) effects and Z3
       ;;gdscript          ; the language you waited for
       ;;(go +lsp)         ; the hipster dialect
       ;;(graphql +lsp)    ; Give queries a REST
       (haskell +lsp)    ; a language that's lazier than I am
       ;;hy                ; readability of scheme w/ speed of python
       ;;idris             ; a language you can depend on
       json              ; At least it ain't XML
       ;;janet             ; Fun fact: Janet is me!
       ;;(java +lsp)       ; the poster child for carpal tunnel syndrome
       javascript        ; all(hope(abandon(ye(who(enter(here))))))
       ;;julia             ; a better, faster MATLAB
       ;;kotlin            ; a better, slicker Java(Script)
       latex             ; writing papers in Emacs has never been so fun
       ;;lean              ; for folks with too much to prove
       ;;ledger            ; be audit you can be
       ;;lua               ; one-based indices? one-based indices
       markdown          ; writing docs for people to ignore
       ;;nim               ; python + lisp at the speed of c
       ;;nix               ; I hereby declare "nix geht mehr!"
       ;;ocaml             ; an objective camel
       (org               ; organize your plain life in plain text
         +attach
         +babel
         +capture
         +export
         +roam
         ;; +present
       )
       ;; php               ; perl's insecure younger brother
       ;;plantuml          ; diagrams for confusing people more
       ;;graphviz          ; diagrams for confusing yourself even more
       ;;purescript        ; javascript, but functional
       python            ; beautiful is better than ugly
       ;;qt                ; the 'cutest' gui framework ever
       ;;racket            ; a DSL for DSLs
       ;;raku              ; the artist formerly known as perl6
       ;;rest              ; Emacs as a REST client
       ;;rst               ; ReST in peace
       (ruby +rails)     ; 1.step {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"}
       (rust +lsp)       ; Fe2O3.unwrap().unwrap().unwrap().unwrap()
       ;;scala             ; java, but good
       ;;(scheme +guile)   ; a fully conniving family of lisps
       sh                ; she sells {ba,z,fi}sh shells on the C xor
       ;;sml
       ;;solidity          ; do you need a blockchain? No.
       swift             ; who asked for emoji variables?
       ;;terra             ; Earth and Moon in alignment for performance.
       web               ; the tubes
       yaml              ; JSON, but readable
       ;;zig               ; C, but simpler

       :email
       ;;(mu4e +org +gmail)
       ;;notmuch
       ;;(wanderlust +gmail)

       :app
       ;;calendar
       ;;emms
       ;;everywhere        ; *leave* Emacs!? You must be joking
       ;;irc               ; how neckbeards socialize
       ;;(rss +org)        ; emacs as an RSS reader

       :config
       ;;literate
       (default +bindings +smartparens)) ; TODO +snippets +evil-commands
packages.el
;; -*- no-byte-compile: t; -*-
;;; $DOOMDIR/packages.el

;; To install a package with Doom you must declare them here and run 'doom sync'
;; on the command line, then restart Emacs for the changes to take effect -- or
;; use 'M-x doom/reload'.

;; To install SOME-PACKAGE from MELPA, ELPA or emacsmirror:
;; (package! some-package)

;; org
(package! org-appear)
(package! org-transclusion)

;; citations and bibliography
(package! citar)
(package! citar-org-roam)

;; pdf annotation
(package! org-noter)

;; roam graph visualization
(package! org-roam-ui)

;; glossary and acronym management
(package! org-glossary
  :recipe (:host github :repo "tecosaur/org-glossary"))

;; structural (AST-aware) diffs on demand from magit
(package! difftastic)


;; To install a package directly from a remote git repo, you must specify a
;; `:recipe'. You'll find documentation on what `:recipe' accepts here:
;; https://github.com/radian-software/straight.el#the-recipe-format
;; (package! another-package
;;   :recipe (:host github :repo "username/repo"))

;; If the package you are trying to install does not contain a PACKAGENAME.el
;; file, or is located in a subdirectory of the repo, you'll need to specify
;; `:files' in the `:recipe':
;; (package! this-package
;;   :recipe (:host github :repo "username/repo"
;;            :files ("some-file.el" "src/lisp/*.el")))

;; If you'd like to disable a package included with Doom, you can do so here
;; with the `:disable' property:
;; (package! builtin-package :disable t)

;; You can override the recipe of a built in package without having to specify
;; all the properties for `:recipe'. These will inherit the rest of its recipe
;; from Doom or MELPA/ELPA/Emacsmirror:
;; (package! builtin-package :recipe (:nonrecursive t))
;; (package! builtin-package-2 :recipe (:repo "myfork/package"))

;; Specify a `:branch' to install a package from a particular branch or tag.
;; This is required for some packages whose default branch isn't 'master' (which
;; our package manager can't deal with; see radian-software/straight.el#279)
;; (package! builtin-package :recipe (:branch "develop"))

;; Use `:pin' to specify a particular commit to install.
;; (package! builtin-package :pin "1a2b3c4d5e")


;; Doom's packages are pinned to a specific commit and updated from release to
;; release. The `unpin!' macro allows you to unpin single packages...
;; (unpin! pinned-package)
;; ...or multiple packages
;; (unpin! pinned-package another-pinned-package)
;; ...Or *all* packages (NOT RECOMMENDED; will likely break things)
;; (unpin! t)
Snippets
# name: Full name: Anton Sebastian Bilbaeno
# key: asb
# --
Anton Sebastian Bilbaeno

Tealdeer

Community-maintained tldr pages. Quick command reference.

brew "tealdeer"
# Tealdeer
export TEALDEER_CONFIG_DIR=$HOME/.config/tealdeer
[style.description]
underline = false
bold = false
italic = false

[style.command_name]
foreground = "cyan"
underline = false
bold = false
italic = false

[style.example_text]
foreground = "green"
underline = false
bold = false
italic = false

[style.example_code]
foreground = "cyan"
underline = false
bold = false
italic = false

[style.example_variable]
foreground = "cyan"
underline = true
bold = false
italic = false

[display]
compact = false
use_pager = false

[updates]
auto_update = true
auto_update_interval_hours = 720

[directories]

mise

Version manager for dev tools

brew "mise"
# mise
eval "$(mise activate zsh)"

Haskell

# haskell
export PATH="$HOME/.ghcup/bin:$PATH"
export PATH="$HOME/.local/bin:$PATH"

JankyBorders

Window borders for the tiling window manager.

tap "FelixKratz/formulae", trusted: { formula: "borders" }
brew "FelixKratz/formulae/borders"

Zed

Installation

cask "zed"

Settings

// Zed settings
//
// For information on how to configure Zed, see the Zed
// documentation: https://zed.dev/docs/configuring-zed
//
// To see all of Zed's default settings without changing your
// custom settings, run `zed: open default settings` from the
// command palette (cmd-shift-p / ctrl-shift-p)
{
  "agent_servers": {
    "codex-acp": {
      "default_config_options": {
        "reasoning_effort": "low"
      },
      "type": "registry"
    }
  },
  "indent_guides": {
    "line_width": 1
  },
  "edit_predictions": {
    "allow_data_collection": "default",
    "mode": "subtle",
    "provider": "zed"
  },
  "context_servers": {
    "mcp-server-github": {
      "enabled": false,
      "remote": false,
      "settings": {}
    },
    "kagimcp": {
      "enabled": false,
      "remote": false,
      "settings": {}
    },
    "mcp-server-context7": {
      "enabled": false,
      "settings": {}
    }
  },
  "soft_wrap": "editor_width",
  "agent": {
    "sandbox_permissions": {
      "allow_fs_write_all": true
    },
    "single_file_review": true,
    "show_turn_stats": true,
    "tool_permissions": {
      "default": "allow"
    },
    "play_sound_when_agent_done": "never",
    "default_profile": "write",
    "default_model": {
      "effort": "low",
      "enable_thinking": true,
      "provider": "openai-subscribed",
      "model": "gpt-5.6-sol"
    },
    "model_parameters": []
  },
  "ui_font_size": 18.0,
  "buffer_font_size": 15.0,
  "theme": {
    "mode": "system",
    "light": "One Light",
    "dark": "One Dark"
  },
  "vim_mode": true,
  "relative_line_numbers": "enabled"
}

Keymap

// Zed keymap
//
// For information on binding keys, see the Zed
// documentation: https://zed.dev/docs/key-bindings
//
// To see the default key bindings run `zed: open default keymap`
// from the command palette.
[
  {
    "context": "Workspace",
    "bindings": {
      // "shift shift": "file_finder::Toggle"
    }
  },
  {
    "context": "Editor && vim_mode == insert",
    "bindings": {
      "j k": "vim::NormalBefore"
    }
  }
  // {
  //   "context": "Editor && vim_mode == normal && vim_operator == none",
  //   "bindings": {
  //     "n": "vim::Left",
  //     "e": "vim::Down",
  //     "i": "vim::Up",
  //     "o": "vim::Right"
  //   }
  // }
]

Ghostty

Terminal emulator.

cask "ghostty"
# This is the configuration file for Ghostty.
#
# This template file has been automatically created at the following
# path since Ghostty couldn't find any existing config files on your system:
#
#   /Users/antoaenono/Library/Application Support/com.mitchellh.ghostty/config
#
# The template does not set any default options, since Ghostty ships
# with sensible defaults for all options. Users should only need to set
# options that they want to change from the default.
#
# Run `ghostty +show-config --default --docs` to view a list of
# all available config options and their default values.
#
# Additionally, each config option is also explained in detail
# on Ghostty's website, at https://ghostty.org/docs/config.

# Config syntax crash course
# ==========================
# # The config file consists of simple key-value pairs,
# # separated by equals signs.
# font-family = Iosevka
# window-padding-x = 2
#
# # Spacing around the equals sign does not matter.
# # All of these are identical:
# key=value
# key= value
# key =value
# key = value
#
# # Any line beginning with a # is a comment. It's not possible to put
# # a comment after a config option, since it would be interpreted as a
# # part of the value. For example, this will have a value of "#123abc":
# background = #123abc
#
# # Empty values are used to reset config keys to default.
# key =
#
# # Some config options have unique syntaxes for their value,
# # which is explained in the docs for that config option.
# # Just for example:
# resize-overlay-duration = 4s 200ms

font-size=28

theme = Monokai Pro Machine

window-save-state = always
macos-titlebar-style = transparent
copy-on-select = clipboard

keybind = cmd+s>r=reload_config

Utilities

cask "keycastr"
brew "gnupg"
brew "pinentry-mac"

Espanso

cask "espanso"

On first install, espanso defaults to ~/Library/Application Support/espanso.
Move it so chezmoi can manage it via ~/.config/espanso:

espanso stop
mv "$HOME/Library/Application Support/espanso" "$HOME/.config/espanso"

Config and snippets are tangled from Cross-platform > Espanso.

Cross-platform

Espanso

Cross-platform text expander.

Installation
Config
# Espanso default configuration
# https://espanso.org/docs/configuration/basics/
backend: auto
Matches
# trigger: abbreviation you type
# replace: expanded text
matches:
  - trigger: ";hello"
    replace: "Hello, how are you?"

  - trigger: "--dang"
    replace: "--dangerously-skip-permissions"

Agents

Cross-agent instructions, skills, and user-owned hooks live under ~/.agents/.
Product-specific directories contain only adapters and configuration that cannot
be shared.

Repository Instructions
# Dotfiles Repository Guidance

## Source of Truth

- `config.org` is a literate configuration file and the authoritative source for configuration and documentation.
- Do not edit tangled files directly. Instead, configuration code should be tangled from an Org code block.
- Keep generated paths and their `:tangle` targets aligned.
- Prose outside Org source blocks is human-authored and must not be edited by agents.

## Change workflow

1. Write configuration code only in Org source blocks in `config.org`. Ask the user to make any needed prose or heading changes.
2. Follow the globally installed `$configure-dotfiles` skill to tangle, review, and apply the change.
3. Review the generated diff, including file modes and templates.
4. Run the relevant validation or dry-run before applying with `chezmoi apply`.

## Safety

- Preserve existing user changes and inspect unexpected diffs before overwriting anything.
- Never place secrets, tokens, private keys, or machine-specific credentials in this repository.
- Prefer small, reversible changes and document non-obvious platform assumptions.

## Agent skill

For dotfiles changes, follow `dot_agents/skills/configure-dotfiles/SKILL.md`.
Global Instructions

The canonical global instructions merge the existing personal guidance used by
different coding agents.

You are capable of excellent work; favor results that are correct, simple, coherent, and well validated.

## Resolving Ambiguity & Clarifying Information

- Ask follow-up questions before implementation when ambiguity affects correctness, scope, domain language, public contracts, data loss, irreversible changes, or durable architectural decisions.
- For low-risk ambiguity, make a reasonable assumption, state it briefly, and proceed.
- If the requested outcome conflicts with existing domain language or architecture, clarify the intended direction before changing code.

## Anti-Sycophancy & Epistemic Stability

- Be helpful without being agreeable by default. Do not validate a user's claim, premise, plan, or interpretation merely because they stated it confidently or repeatedly.
- Reason independently from the user's stated preference. Treat confidence, repetition, social proof, authority, and urgency as signals to re-check evidence, not as reasons to agree.
- Maintain a justified technical stance across turns unless new evidence, code, tests, documentation, or explicit requirements warrant changing it. If you change your view, state what changed and why.
- When pressure or disagreement could be affecting the answer, briefly adopt a neutral reviewer stance: assess the claim as if reviewing a third party's technical argument, then answer in your normal assistant voice.
- Detect false presuppositions and hidden assumptions in requests. Correct them directly before building on the request, especially when they affect correctness, safety, data loss, public contracts, or architecture.
- For ethically risky, biased, unsafe, or discriminatory premises, identify the problematic assumption and redirect to a safer, accurate formulation instead of complying with the premise.
- When the user challenges your answer, re-check the evidence and reasoning. If the original answer remains sound, say so plainly and offer the strongest actionable path forward.
- Separate tone from truth: stay respectful, concise, and collaborative while preserving factual accuracy, engineering rigor, and safety.

# Style

Never use em dashes (—). Prefer commas, colons, semicolons, or regular dashes (-) instead.

# Git

## Branching

Prefer working on a branch rather than committing directly to main. Exception: personal/dotfiles repos where committing to main is acceptable.

## Committing

Review status and diffs before committing. Create small, coherent commits and
stage explicit paths. When changes clearly belong to existing unpushed commits,
consider `git absorb` and inspect the autosquashed history. Never rewrite pushed
history or force-push without explicit permission.

## Commits

Never include "Co-Authored-By" lines in commits.
Never include agent-generation attribution in commits or pull request descriptions.

## Pull request body template

When creating or editing a PR, look for a `PULL_REQUEST_TEMPLATE.md` in `.github/` or the repo root and use it as the body structure. Fill in each section with real content. Reproduce all checklist items, checking off satisfied ones (`[x]`) and leaving the rest unchecked (`[ ]`). Never remove unchecked items or add sections not in the template.
{{ .chezmoi.homeDir }}/.agents/AGENTS.md
{{ .chezmoi.homeDir }}/.agents/AGENTS.md
Hooks

User-owned cross-agent hooks live in ~/.agents/hooks/. Product configuration
must point to scripts there. Externally managed hooks remain product-local so
their installers can update them safely.

The literate configuration reflector snapshots config.org before every Codex
tool call. After the tool completes, it compares the ordered bytes outside Org
source blocks. If those human-authored bytes changed, it restores the complete
file and its previous Git index entry. Repository-local registration keeps this
policy separate from global and externally managed hooks.

{
  "description": "Protect the human-authored prose in the literate dotfiles configuration.",
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 \"$HOME/.agents/hooks/protect_literate_config.py\"",
            "timeout": 10,
            "statusMessage": "Snapshotting literate configuration prose"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 \"$HOME/.agents/hooks/protect_literate_config.py\"",
            "timeout": 10,
            "statusMessage": "Checking literate configuration prose"
          }
        ]
      }
    ]
  }
}
"""Restore config.org when a Codex tool call changes human-authored prose."""

from __future__ import annotations

import hashlib
import json
import os
import re
import shutil
import stat
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
from typing import Any

TARGET_PATH = "config.org"
SNAPSHOT_VERSION = 1
STALE_AFTER_SECONDS = 24 * 60 * 60
BEGIN_SOURCE = re.compile(br"[ \t]*#\+begin_src(?:[ \t].*)?", re.IGNORECASE)
END_SOURCE = re.compile(br"[ \t]*#\+end_src[ \t]*", re.IGNORECASE)


class GuardError(RuntimeError):
    """The protected state could not be snapshotted or restored safely."""


def main() -> int:
    if sys.argv[1:] == ["--self-test"]:
        suite = unittest.defaultTestLoader.loadTestsFromTestCase(GuardTests)
        return 0 if unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful() else 1

    try:
        payload = json.load(sys.stdin)
        cwd = Path(str(payload.get("cwd") or ".")).resolve()
        repo = find_repository(cwd)
        if repo is None:
            return 0

        tool_use_id = str(payload.get("tool_use_id") or "")
        if not tool_use_id:
            raise GuardError("hook input has no tool_use_id")

        root = snapshot_root(repo)
        root.mkdir(parents=True, exist_ok=True, mode=0o700)
        cleanup_stale_snapshots(root)
        snapshot = root / safe_id(tool_use_id)
        event = payload.get("hook_event_name")

        if event == "PreToolUse":
            create_snapshot(repo, snapshot, payload)
            return 0
        if event == "PostToolUse":
            message = validate_and_restore(repo, snapshot, tool_use_id)
            if message:
                print(message, file=sys.stderr)
                return 2
            return 0
        raise GuardError(f"unsupported hook event: {event!r}")
    except (GuardError, json.JSONDecodeError, OSError, subprocess.CalledProcessError) as error:
        print(f"Literate config reflector failed: {error}", file=sys.stderr)
        return 2


def find_repository(cwd: Path) -> Path | None:
    try:
        result = run_git(cwd, "rev-parse", "--show-toplevel")
    except subprocess.CalledProcessError:
        return None
    return Path(result.stdout.decode().strip()).resolve()


def run_git(
    repo: Path, *args: str, input: bytes | None = None
) -> subprocess.CompletedProcess[bytes]:
    return subprocess.run(
        ["git", "-C", str(repo), *args],
        input=input,
        check=True,
        capture_output=True,
    )


def snapshot_root(repo: Path) -> Path:
    result = run_git(repo, "rev-parse", "--git-path", "codex-hooks/protect-literate-config")
    path = Path(result.stdout.decode().strip())
    return path if path.is_absolute() else (repo / path).resolve()


def safe_id(value: str) -> str:
    return hashlib.sha256(value.encode()).hexdigest()


def protected_projection(content: bytes) -> bytes:
    """Return exact ordered bytes outside complete Org source blocks."""

    protected = bytearray()
    inside_source = False
    source_started_at = 0

    for line_number, line in enumerate(content.splitlines(keepends=True), 1):
        logical_line = line.rstrip(b"\r\n")
        if inside_source:
            if END_SOURCE.fullmatch(logical_line):
                inside_source = False
            continue
        if END_SOURCE.fullmatch(logical_line):
            raise GuardError(f"unmatched #+end_src at line {line_number}")
        if BEGIN_SOURCE.fullmatch(logical_line):
            inside_source = True
            source_started_at = line_number
            continue
        protected.extend(line)

    if inside_source:
        raise GuardError(f"unclosed #+begin_src at line {source_started_at}")
    return bytes(protected)


def cleanup_stale_snapshots(root: Path) -> None:
    cutoff = time.time() - STALE_AFTER_SECONDS
    for child in root.iterdir():
        try:
            if child.is_dir() and child.stat().st_mtime < cutoff:
                shutil.rmtree(child)
        except OSError:
            pass


def create_snapshot(repo: Path, destination: Path, payload: dict[str, Any]) -> None:
    target = repo / TARGET_PATH
    info = target.lstat()
    if not stat.S_ISREG(info.st_mode):
        raise GuardError(f"{TARGET_PATH} is not a regular file")

    content = target.read_bytes()
    projection = protected_projection(content)
    if destination.exists():
        shutil.rmtree(destination)
    temporary = destination.with_name(f".{destination.name}.{os.getpid()}.tmp")
    if temporary.exists():
        shutil.rmtree(temporary)
    temporary.mkdir(parents=True, mode=0o700)

    metadata = {
        "version": SNAPSHOT_VERSION,
        "created_at": time.time(),
        "repo": str(repo),
        "tool_use_id": payload.get("tool_use_id"),
        "session_id": payload.get("session_id"),
        "turn_id": payload.get("turn_id"),
        "mode": stat.S_IMODE(info.st_mode),
        "content_sha256": digest(content),
        "projection_sha256": digest(projection),
    }
    write_private(temporary / "config.org", content)
    write_private(
        temporary / "manifest.json",
        json.dumps(metadata, sort_keys=True, separators=(",", ":")).encode(),
    )
    write_private(temporary / "index.entries", protected_index(repo))
    write_private(temporary / "integrity.sha256", snapshot_integrity(temporary).encode())
    os.replace(temporary, destination)


def validate_and_restore(repo: Path, snapshot: Path, tool_use_id: str) -> str | None:
    if not snapshot.is_dir():
        raise GuardError("matching pre-tool snapshot is missing")

    recorded_integrity = (snapshot / "integrity.sha256").read_text()
    if snapshot_integrity(snapshot) != recorded_integrity:
        raise GuardError("snapshot content or metadata was modified")

    metadata = json.loads((snapshot / "manifest.json").read_text())
    if (
        metadata.get("version") != SNAPSHOT_VERSION
        or metadata.get("repo") != str(repo)
        or metadata.get("tool_use_id") != tool_use_id
    ):
        raise GuardError("snapshot metadata does not match this tool call")

    before = (snapshot / "config.org").read_bytes()
    if digest(before) != metadata.get("content_sha256"):
        raise GuardError("snapshotted config.org content was modified")
    before_projection = protected_projection(before)
    if digest(before_projection) != metadata.get("projection_sha256"):
        raise GuardError("snapshotted prose projection was modified")

    target = repo / TARGET_PATH
    after_projection: bytes | None = None
    try:
        if target.is_file() and not target.is_symlink():
            after_projection = protected_projection(target.read_bytes())
    except GuardError:
        after_projection = None

    if after_projection == before_projection:
        shutil.rmtree(snapshot)
        return None

    restore_file(target, before, int(metadata["mode"]))
    restore_index(repo, (snapshot / "index.entries").read_bytes())
    shutil.rmtree(snapshot)
    return (
        "Restored config.org because this model tool call changed human-authored "
        "content outside Org source blocks. Retry with a source-block-only edit."
    )


def digest(content: bytes) -> str:
    return hashlib.sha256(content).hexdigest()


def write_private(path: Path, content: bytes) -> None:
    descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(descriptor, "wb") as stream:
        stream.write(content)


def snapshot_integrity(snapshot: Path) -> str:
    checksum = hashlib.sha256()
    for name in ("config.org", "manifest.json", "index.entries"):
        checksum.update(name.encode())
        checksum.update(b"\0")
        checksum.update((snapshot / name).read_bytes())
        checksum.update(b"\0")
    return checksum.hexdigest()


def protected_index(repo: Path) -> bytes:
    return run_git(repo, "ls-files", "--stage", "-z", "--", TARGET_PATH).stdout


def restore_file(target: Path, content: bytes, mode: int) -> None:
    target.parent.mkdir(parents=True, exist_ok=True)
    descriptor, temporary_name = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
    temporary = Path(temporary_name)
    try:
        with os.fdopen(descriptor, "wb") as stream:
            stream.write(content)
            stream.flush()
            os.fsync(stream.fileno())
        os.chmod(temporary, mode)
        os.replace(temporary, target)
    finally:
        if temporary.exists():
            temporary.unlink()


def restore_index(repo: Path, entries: bytes) -> None:
    run_git(repo, "update-index", "--force-remove", "--", TARGET_PATH)
    if entries:
        run_git(repo, "update-index", "-z", "--index-info", input=entries)


class GuardTests(unittest.TestCase):
    def setUp(self) -> None:
        self.temporary = tempfile.TemporaryDirectory()
        self.repo = Path(self.temporary.name)
        run_git(self.repo, "init", "--quiet")
        run_git(self.repo, "config", "user.email", "[email protected]")
        run_git(self.repo, "config", "user.name", "Lit Test")
        self.original = (
            b"* Human heading\nHuman prose.\n"
            b"#+begin_src shell :tangle ./example\n"
            b"echo before\n"
            b"#+end_src\n"
            b"Closing prose.\n"
        )
        (self.repo / TARGET_PATH).write_bytes(self.original)
        run_git(self.repo, "add", TARGET_PATH)
        run_git(self.repo, "commit", "--quiet", "-m", "initial")

    def tearDown(self) -> None:
        self.temporary.cleanup()

    def snapshot(self, tool_use_id: str = "tool-1") -> Path:
        destination = snapshot_root(self.repo) / safe_id(tool_use_id)
        destination.parent.mkdir(parents=True, exist_ok=True)
        create_snapshot(
            self.repo,
            destination,
            {"tool_use_id": tool_use_id, "session_id": "session", "turn_id": "turn"},
        )
        return destination

    def run_hook(self, event: str, tool_use_id: str = "tool-1") -> subprocess.CompletedProcess[bytes]:
        payload = {
            "cwd": str(self.repo),
            "hook_event_name": event,
            "tool_use_id": tool_use_id,
            "session_id": "session",
            "turn_id": "turn",
        }
        return subprocess.run(
            [sys.executable, str(Path(__file__).resolve())],
            input=json.dumps(payload).encode(),
            capture_output=True,
        )

    def test_projection_ignores_source_blocks(self) -> None:
        changed = self.original.replace(b"echo before", b"echo after\necho another")
        self.assertEqual(protected_projection(self.original), protected_projection(changed))

    def test_projection_detects_prose_changes(self) -> None:
        changed = self.original.replace(b"Human prose.", b"Model prose.")
        self.assertNotEqual(protected_projection(self.original), protected_projection(changed))

    def test_projection_rejects_malformed_source_blocks(self) -> None:
        with self.assertRaisesRegex(GuardError, "unclosed"):
            protected_projection(b"Human prose.\n#+begin_src shell\necho nope\n")
        with self.assertRaisesRegex(GuardError, "unmatched"):
            protected_projection(b"Human prose.\n#+end_src\n")

    def test_source_only_edit_survives(self) -> None:
        snapshot = self.snapshot()
        changed = self.original.replace(b"echo before", b"echo after")
        (self.repo / TARGET_PATH).write_bytes(changed)

        self.assertIsNone(validate_and_restore(self.repo, snapshot, "tool-1"))
        self.assertEqual((self.repo / TARGET_PATH).read_bytes(), changed)

    def test_prose_edit_restores_file_and_index(self) -> None:
        snapshot = self.snapshot()
        changed = self.original.replace(b"Human prose.", b"Model prose.").replace(
            b"echo before", b"echo after"
        )
        (self.repo / TARGET_PATH).write_bytes(changed)
        run_git(self.repo, "add", TARGET_PATH)

        message = validate_and_restore(self.repo, snapshot, "tool-1")

        self.assertIn("Restored config.org", message or "")
        self.assertEqual((self.repo / TARGET_PATH).read_bytes(), self.original)
        self.assertEqual(run_git(self.repo, "diff", "--cached", "--", TARGET_PATH).stdout, b"")

    def test_malformed_edit_restores_file(self) -> None:
        snapshot = self.snapshot()
        (self.repo / TARGET_PATH).write_bytes(b"Human prose.\n#+begin_src shell\necho nope\n")

        self.assertIsNotNone(validate_and_restore(self.repo, snapshot, "tool-1"))
        self.assertEqual((self.repo / TARGET_PATH).read_bytes(), self.original)

    def test_staging_preexisting_human_prose_is_allowed(self) -> None:
        human = self.original.replace(b"Human prose.", b"Human revision.")
        (self.repo / TARGET_PATH).write_bytes(human)
        snapshot = self.snapshot()
        run_git(self.repo, "add", TARGET_PATH)

        self.assertIsNone(validate_and_restore(self.repo, snapshot, "tool-1"))
        self.assertEqual(run_git(self.repo, "show", f":{TARGET_PATH}").stdout, human)

    def test_deleted_file_is_restored(self) -> None:
        snapshot = self.snapshot()
        (self.repo / TARGET_PATH).unlink()
        run_git(self.repo, "update-index", "--force-remove", "--", TARGET_PATH)

        self.assertIsNotNone(validate_and_restore(self.repo, snapshot, "tool-1"))
        self.assertEqual((self.repo / TARGET_PATH).read_bytes(), self.original)
        self.assertEqual(run_git(self.repo, "show", f":{TARGET_PATH}").stdout, self.original)

    def test_hook_entrypoint_restores_deleted_file(self) -> None:
        self.assertEqual(self.run_hook("PreToolUse").returncode, 0)
        (self.repo / TARGET_PATH).unlink()

        result = self.run_hook("PostToolUse")

        self.assertEqual(result.returncode, 2)
        self.assertIn(b"Restored config.org", result.stderr)
        self.assertEqual((self.repo / TARGET_PATH).read_bytes(), self.original)


if __name__ == "__main__":
    raise SystemExit(main())
omp (oh-my-pi)

The omp coding agent, installed globally via bun add -g @oh-my-pi/pi-coding-agent.
Bun drops its global binaries in ~/.bun/bin, so that needs to be on PATH.

# omp (oh-my-pi) / bun global bins
export PATH="$HOME/.bun/bin:$PATH"
Skills

Cross-agent skills are tangled to ~/.agents/skills/. Product adapters point to
the canonical skill directories.

configure-dotfiles
---
name: configure-dotfiles
description: Safely maintain personal dotfiles in the chezmoi-managed, Org-babel-tangled source repository. Use from any conversation or working directory to inspect, add, change, tangle, preview, or apply machine configuration.
---

# Configure Dotfiles

## Model

Run `chezmoi source-path` to locate the source repository. Use that directory as
the working directory for the entire task.

`config.org` is the source of truth. Files such as `dot_config/**`, `Brewfile`, and
`.chezmoiscripts/**` are tangled outputs. Keep prose and code together in the Org
document. Never edit a tangled output directly.

## Workflow

1. Inspect `git status --short`, the relevant Org section, and its current tangle
   target. Preserve unrelated user changes.
2. Edit only Org source blocks in `config.org`, including the exact `:tangle`
   target. New blocks may be added under an existing human-authored section. Ask
   the user to make any required prose or heading changes. Add a shebang when the
   target must be executable.
3. Run `~/.agents/skills/configure-dotfiles/scripts/tangle.sh`. It uses a running
   Emacs server when available and otherwise launches batch Emacs. Do not
   reproduce the underlying Emacs invocation manually.
4. Run `git diff --check`, `git status --short`, and `git diff`. Confirm every
   generated change follows from the requested Org edits.
5. Preview deployment with `chezmoi apply --dry-run --verbose --force`. On normal
   command-line workflows, use `--force` only with `--dry-run`; it reveals target
   drift without overwriting it.
6. If implementation was requested and the preview contains only intended
   changes, apply the narrowest affected target or directory with `chezmoi apply
   --verbose <target>`. The interactive `M-x tangle-moi` helper may use `--force`
   for its real apply only after showing the full forced dry-run and receiving
   explicit confirmation. This avoids a second prompt that requires a TTY.
7. Verify installed targets and report source changes, applied targets, and
   anything intentionally left unapplied.

`M-x tangle-moi` is the interactive interface to the same tangle core installed
at `~/.config/doom/lisp/tangle-moi.el`.

## Stop conditions

Stop if the tangle script reports unsaved changes in an Emacs buffer visiting
`config.org`; preserve that buffer and ask the user to resolve it. Ask before
applying when the preview would overwrite independently modified home files,
delete targets, change permissions unexpectedly, execute package or setup
scripts, or include changes outside the request. Do not apply when the user asked
only for review or diagnosis.

## Conventions

- Use chezmoi naming conventions for destination paths (`dot_`, `private_`,
  `symlink_`, and `.tmpl`).
- Keep platform-specific scripts under `.chezmoiscripts/<os>/` and guard them
  appropriately.
- Keep secrets and host-specific credentials out of the repository.
- Prefer idempotent scripts with `set -e` and explicit paths.
- Flag nearby prose that needs a human update; do not edit it.

## Validation checklist

- The Org source parses and tangles successfully.
- The generated diff contains no unrelated changes.
- Templates and scripts remain executable where intended.
- The chezmoi dry-run reports only expected target changes.
set -eu

elisp_file="${XDG_CONFIG_HOME:-$HOME/.config}/doom/lisp/tangle-moi.el"

if [ ! -r "$elisp_file" ]; then
  echo "Tangle failed: shared Emacs library not found at $elisp_file." >&2
  exit 1
fi

if command -v emacsclient >/dev/null 2>&1 \
    && emacsclient --eval t >/dev/null 2>&1; then
  escaped_elisp_file=$(printf '%s' "$elisp_file" \
    | sed 's/\\/\\\\/g; s/"/\\"/g')
  exec emacsclient --eval \
    "(progn (load \"$escaped_elisp_file\" nil t) (tangle-moi-tangle))"
fi

if ! command -v emacs >/dev/null 2>&1; then
  echo "Tangle failed: Emacs is not installed or not on PATH." >&2
  exit 127
fi

exec emacs --batch \
  --load "$elisp_file" \
  --funcall tangle-moi-tangle-batch
interface:
  display_name: "Configure Dotfiles"
  short_description: "Safely edit, tangle, and apply personal dotfiles"
  default_prompt: "Use $configure-dotfiles to update my literate dotfiles and safely apply the result."
{{ .chezmoi.homeDir }}/.agents/skills/configure-dotfiles
tangle-moi
---
name: tangle-moi
description: Tangle the literate chezmoi config, preview the target diff, and optionally apply it. Use when explicitly invoking the agent equivalent of M-x tangle-moi.
---

# Tangle Moi

Use this skill as the explicit manual shortcut for the `M-x tangle-moi`
workflow.

Read `~/.agents/skills/configure-dotfiles/SKILL.md` and follow its workflow.
Run `~/.agents/skills/tangle-moi/tangle.sh` for the tangle step, preview the
chezmoi changes, and always ask before applying. Never auto-apply.
set -eu
exec "$HOME/.agents/skills/configure-dotfiles/scripts/tangle.sh"
interface:
  display_name: "Tangle Moi"
  short_description: "Tangle and preview the literate dotfiles config"
  default_prompt: "Use $tangle-moi to tangle my literate dotfiles and preview the chezmoi changes."
{{ .chezmoi.homeDir }}/.agents/skills/tangle-moi

git

aliases

oh-my-zsh git plugin comes with many aliases:
run alias to view all
also handy to alias | grep rebase

git-absorb

git-absorb (opens in a new tab) automatically creates fixup commits for local changes and assigns
them to the commits they amend. It is useful for cleaning up a branch before
review or merge.

brew "git-absorb"

github

SSH
authentication
signing

GPG

brew install gnupg
gpg --full-generate-key
gpg (GnuPG) 2.4.8; Copyright (C) 2025 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (9) ECC (sign and encrypt) *default*
  (10) ECC (sign only)
  (14) Existing key from card
Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072) 4096
Requested keysize is 4096 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: Anton Bilbaeno
Email address: [email protected]
Comment:
You selected this USER-ID:
    "Anton Bilbaeno <[email protected]>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: /Users/antoaenono/.gnupg/trustdb.gpg: trustdb created
gpg: directory '/Users/antoaenono/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/Users/antoaenono/.gnupg/openpgp-revocs.d/393A*.rev'
public and secret key created and signed.

pub   rsa4096 2025-10-19 [SC]
      393A*
uid                      Anton Bilbaeno <[email protected]>
sub   rsa4096 2025-10-19 [E]

Then !!C-x C-f!! ./authinfo.gpg, write.
Trying to access again from Emacs fails, bcuz gpg subprocess isn't connected.

brew install pinentry-mac

Add to ~/.gnupg/gpg-agent.conf

pinentry-program /opt/homebrew/bin/pinentry-mac

Make sure agent picks up config

gpg-connect-agent reloadagent /bye

Now when accessing ./authinfo.gpg, dialog should appear for passphrase.

Footnotes

1

Donald Knuth introduced literate programming in 1984.

2

The Org document remains the source of truth.

3

Keep prose and code together in config.org.