GNU Emacs Configuration

I’ve recently moved from a literate Emacs configuration based on Org mode to a simpler init.el file, reproduced below:

Main configuration

;;; init.el --- Personal Emacs configuration -*- lexical-binding: t -*-
;; Copyright (C) 2021-2024 Eshel Yaron

;; Author: Eshel Yaron <[email protected]>

;;; Commentary:

;; My personal Emacs configuration

;;; Code:

;;; Temporarily increase GC threshold to expedite Emacs startup

(let ((normal-gc-cons-threshold (* 20 1024 1024))
      (init-gc-cons-threshold (* 1024 1024 1024)))
  (setq gc-cons-threshold init-gc-cons-threshold)
  (add-hook 'after-init-hook
            (lambda ()
              (setq gc-cons-threshold normal-gc-cons-threshold))))

;;; OS-specific settings

(pcase system-type
  ('darwin
   (add-to-list 'exec-path "/usr/local/bin")
   (setq initial-frame-alist '((fullscreen . fullboth)
                               ;; (minibuffer)
                               )
         frame-title-format "Emacs")
   (set-fontset-font t '(?􀀀 . ?􏿽) "SF Pro Display"))
  ('android
   (tool-bar-mode)
   (modifier-bar-mode)
   (visual-line-mode)
   (setq initial-frame-alist '((tool-bar-position . bottom)))))

;;; Check for external programs

(unless (eq system-type 'android)
  (dolist (program '("autoconf"
                     "automake"
                     "aws"
                     "bash"
                     "cmake"
                     "gcc"
                     "git"
                     "gpg"
                     "go"
                     "gopls"
                     "gtar"
                     "convert"
                     "ispell"
                     "jq"
                     "mutool"
                     "ninja"
                     "psql"
                     "rg"
                     "stow"
                     "makeinfo"
                     "tree-sitter"
                     "pandoc"
                     "mpv"))
    (unless (executable-find program)
      (display-warning 'programming
                       (format "Missing external program \"%s\"" program)
                       :error))))

;;; Set up Elpaca

(defvar elpaca-installer-version 0.9)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
                              :ref nil :depth 1 :inherit ignore
                              :files (:defaults "elpaca-test.el" (:exclude "extensions"))
                              :build (:not elpaca--activate-package)))
(let* ((repo  (expand-file-name "elpaca/" elpaca-repos-directory))
       (build (expand-file-name "elpaca/" elpaca-builds-directory))
       (order (cdr elpaca-order))
       (default-directory repo))
  (add-to-list 'load-path (if (file-exists-p build) build repo))
  (unless (file-exists-p repo)
    (make-directory repo t)
    (when (< emacs-major-version 28) (require 'subr-x))
    (condition-case-unless-debug err
        (if-let* ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
                  ((zerop (apply #'call-process `("git" nil ,buffer t "clone"
                                                  ,@(when-let* ((depth (plist-get order :depth)))
                                                      (list (format "--depth=%d" depth) "--no-single-branch"))
                                                  ,(plist-get order :repo) ,repo))))
                  ((zerop (call-process "git" nil buffer t "checkout"
                                        (or (plist-get order :ref) "--"))))
                  (emacs (concat invocation-directory invocation-name))
                  ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
                                        "--eval" "(byte-recompile-directory \".\" 0 'force)")))
                  ((require 'elpaca))
                  ((elpaca-generate-autoloads "elpaca" repo)))
            (progn (message "%s" (buffer-string)) (kill-buffer buffer))
          (error "%s" (with-current-buffer buffer (buffer-string))))
      ((error) (warn "%s" err) (delete-directory repo 'recursive))))
  (unless (require 'elpaca-autoloads nil t)
    (require 'elpaca)
    (elpaca-generate-autoloads "elpaca" repo)
    (load "./elpaca-autoloads")))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))

;;; Install packages

(elpaca org-transclusion)
(elpaca sweeprolog)
(elpaca (bbdb
         :repo "https://git.savannah.nongnu.org/git/bbdb.git"
         :files (:defaults "lisp/*.el.in")
         :pre-build (("./autogen.sh")
                     ("./configure")
                     ("make"))))
(elpaca debbugs)
(elpaca diff-hl)
(elpaca (eat
         :pre-build (("make" "terminfo")
                     ("makeinfo" "--no-split" "eat.texi")
                     ("touch" "dir")
                     ("install-info" "--dir=dir" "eat.info"))))
(elpaca (elfeed
         :pre-build (("pandoc" "-o" "elfeed.texi" "README.md"))))
(elpaca emms)
(elpaca htmlize)
;; (elpaca (oauth2
;;          :repo "git://git.sv.gnu.org/emacs/elpa"
;;          :local-repo "oauth2"
;;          :branch "externals/oauth2"))
(elpaca transient)
(elpaca (magit :repo "[email protected]:~eshel/magit"))
(elpaca (el-search :repo "[email protected]:~eshel/el-search" :branch "master"))
(elpaca (markdown-mode
         :pre-build (("pandoc" "-o" "markdown-mode.texi" "README.md"))))
(elpaca mastodon)
(elpaca (osm
         :pre-build (("emacs" "--batch" "-l" "ox-texinfo" "README.org"
                      "--eval" "(setq org-babel-confirm-evaluate-answer-no t)"
                      "-f" "org-texinfo-export-to-texinfo"))))
(elpaca sqlformat)
(elpaca (auctex
         :repo "[email protected]:~eshel/auctex"
         :branch "main"
         :files ("*.el" "*.info*")
         :pre-build (("./autogen.sh")
                     ("./configure" "--with-lispdir=." "--with-texmf-dir=/tmp")
                     ("make")
                     ("cp" "doc/auctex.info" "doc/auctex.info-1" "doc/auctex.info-2" "doc/preview-latex.info" "."))))
(elpaca (tablist :repo "[email protected]:~eshel/tablist"))
(elpaca (pdf-tools
         :repo "[email protected]:~eshel/pdf-tools"
         :pre-build (("emacs" "--batch" "-l" "ox-texinfo" "README.org"
                      "--eval" "(setq org-babel-confirm-evaluate-answer-no t)"
                      "--eval" "(setq org-export-with-broken-links t)"
                      "-f" "org-texinfo-export-to-texinfo")
                     ("mv" "README.texi"  "pdf-tools.texi"))))
(elpaca ob-prolog)
(elpaca (kubed :repo "~/checkouts/kubed"))
;; (elpaca (combobulate :repo "~/checkouts/combobulate"))
;; (elpaca (lean4-mode :repo "https://github.com/leanprover-community/lean4-mode"
;;                     :files ("*")))
(elpaca (ultra-scroll :repo "[email protected]:jdtsmith/ultra-scroll.git"))
;; (elpaca kubed)

(elpaca-wait)

(defvar-keymap esy/elpaca-prefix-map
  :doc "Keymap for `elpaca' commands."
  :prefix 'esy/elpaca-prefix-map
  "r" #'elpaca-rebuild
  "e" #'elpaca-recipe
  "f" #'elpaca-fetch
  "F" #'elpaca-fetch-all
  "u" #'elpaca-merge
  "U" #'elpaca-merge-all
  "v" #'elpaca-visit
  "d" #'elpaca-delete
  "l" #'elpaca-log-updates
  "i" #'elpaca-info
  "t" #'elpaca-try
  "m" #'elpaca-update-menus)

(keymap-unset elpaca-ui-mode-map "p" t)
(keymap-set elpaca-ui-mode-map "l" #'elpaca-ui-mark-pull)

(defun esy/log-diff (id ref)
  (require 'diff-mode)
  (let ((diff-default-read-only t))
    (elpaca-log-diff id ref)))

(setq elpaca-log-diff-function #'esy/log-diff)

(add-hook 'elpaca-log-mode-hook #'elpaca-log-update-mode)
(add-hook 'elpaca-log-mode-hook
          (lambda ()
            (add-hook 'quit-window-hook
                      (lambda ()
                        (when (get-buffer-window "*elpaca-diff*")
                         (quit-windows-on "*elpaca-diff*")))
                      nil t)))

;;; Set some variables

(setq
 ;; my name
 user-full-name "Eshel Yaron"
 ;; my email address
 user-mail-address "[email protected]"
 mail-user-agent 'gnus-user-agent
 read-mail-command 'gnus
 ;; direct Custom definitions to some file and forget about it
 custom-file (expand-file-name "custom.el" user-emacs-directory)
 ;; only display the warning buffer if something's really broken
 warning-minimum-level :error
 ;; don't use stale .elc files
 load-prefer-newer t
 ;; disable popup dialogs
 use-dialog-box nil
 ;; disable splash screen
 inhibit-startup-screen t
 ;; make the scratch message more concise
 initial-scratch-message ";; Go.\n"
 ;; don't ring the bell
 ring-bell-function #'ignore
 ;; make C-x b obey display-buffer-alist
 switch-to-buffer-obey-display-actions t
 ;; disable new mail mode line indication
 display-time-mail-function #'ignore
 display-time-24hr-format t
 ;; enable recursive minibuffers
 enable-recursive-minibuffers t
 minibuffer-beginning-of-buffer-movement t
 bug-reference-url-format "https://debbugs.gnu.org/%s"
 ;; save bookmarks immediately
 bookmark-save-flag 1
 ;; show matches count in isearch prompt
 isearch-lazy-count t
 ;; kill Eat terminal buffers when their process exits
 eat-kill-buffer-on-exit t
 ;; don't spawn new frames in Ediff
 ediff-window-setup-function #'ediff-setup-windows-plain
 duplicate-line-final-position -1
 ;; tramp-kubernetes-namespace "argo"
 ;; configure Org capture templates
 org-capture-templates '(("t" "Todo [inbox]" entry
                          (file+headline "~/org/inbox.org" "Tasks")
                          "** TODO [#B] %^{Task}    %^g
:PROPERTIES:
:CreatedAt: %u
:CapturedAt: %a
:CapturedAs: Inbox Task
:END:"
                          :prepend t
                          :empty-lines 1
                          :immediate-finish t)
                         ("w" "Work [inbox]" entry
                          (file+headline "~/org/inbox.org" "Tasks")
                          "** TODO [#B] %^{Task}    :work:
:PROPERTIES:
:CreatedAt: %u
:CapturedAt: %a
:CapturedAs: Work Task
:END:"
                          :prepend t
                          :empty-lines 1
                          :immediate-finish t)
                         ("c" "New Calendar Event" entry
                          (file+headline "~/org/inbox.org" "Calendar")
                          "** %^{Title}    %^g
:PROPERTIES:
:CreatedAt: %u
:CapturedAt: %a
:CapturedAs: Calendar Event
:END:
%(format-time-string \"<%Y-%m-%d %H:%M\" (org-read-date t t))-%(format-time-string \"%H:%M>\" (org-read-date t t))
%i"
                          :prepend t
                          :empty-lines 1
                          :immediate-finish t)
                         ("h" "Homework" entry
                          (file+headline "~/org/inbox.org" "Tasks")
                          "** TODO [#B] %^{Task}    :studies:
DEADLINE: %(format-time-string \"<%Y-%m-%d %H:%M>\" (org-read-date t t))
:PROPERTIES:
:CreatedAt: %u
:CapturedAt: %a
:CapturedAs: Homework
:END:
%?"
                          :prepend t
                          :empty-lines 1))
 ;; point Org to file agenda file
 org-agenda-files '("~/org/inbox.org")
 org-default-notes-file "~/org/inbox.org"
 ;; weeks start on Sunday
 org-agenda-start-on-weekday 0
 ;; use a nice unicode ellipsis
 org-ellipsis "…"
 ;; open everything withing Emacs
 org-file-apps '((t . emacs))
 ;; enable Org speed commands
 org-use-speed-commands t
 ;; task states
 org-todo-keywords '((sequence
                      "TODO(t)"
                      "BLOCKED(b@/!)"
                      "INPROGRESS(i!)"
                      "|"
                      "DONE(d!)"
                      "CANCELED(c@)"))
 ;; set a task to INPROGRESS when I clock into it
 org-clock-in-switch-to-state "INPROGRESS"
 ;; log the finish time of tasks in Org
 org-log-done 'time
 org-log-into-drawer t
 ;; minimal prompt for task states
 org-use-fast-todo-selection 'expert
 ;; set up a group of mutual exclusive tags
 org-tag-alist '((:startgroup)
                 ("adi"      . ?a)
                 ("holland"  . ?h)
                 ("work"     . ?w)
                 ("studies"  . ?s)
                 ("esols"    . ?e)
                 ("personal" . ?p)
                 ("kubed"    . ?k)
                 (:endgroup))
 ;; minimal prompt for tags
 org-fast-tag-selection-single-key 'expert
 ;; allow evaluating some languages with Org Babel
 org-babel-load-languages '((emacs-lisp . t)
                            (shell      . t)
                            (sql        . t)
                            (prolog     . t))
 ;; do not ask for confirmation when evaluating Org source blocks
 org-confirm-babel-evaluate nil
 ;; extra modules to load along with Org
 org-modules '(ol-bbdb
               ol-bibtex
               ol-docview
               ol-gnus
               ol-info
               ol-irc
               ol-eww
               ob-sql
               org-tempo)
 ;; refile Org entries to my agenda file(s)
 org-refile-targets '((org-agenda-files . (:maxlevel . 5))
                      (nil . (:maxlevel . 3)))
 ;; use full outline path when prompting for refile target
 org-refile-use-outline-path t
 ;; archive for Org entries
 org-archive-location "~/org/journal.org::datetree/* Finished Tasks   :ARCHIVE:"
 ;; associate Org Babel languages with major modes
 org-src-lang-modes '(("SWI-Prolog" . sweeprolog)
                      ("Dockerfile" . dockerfile-ts)
                      ("bash"       . sh)
                      ("shell"      . sh)
                      ("toml"       . toml-ts)
                      ("C"          . c))
 ;; preview LaTeX fragments in Org buffers via SVG
 org-preview-latex-default-process 'dvisvgm
 ;; don't auto-save remote files
 remote-file-name-inhibit-auto-save t
 ;; don't ask me about it either
 tramp-allow-unsafe-temporary-files t
 remote-file-name-inhibit-locks t
 ;; increase maximum number of recent files Emacs remembers
 recentf-max-saved-items 1024
 ;; increase maximum kill ring size
 kill-ring-max 256
 ;; save text copied from another program to the kill ring
 save-interprogram-paste-before-kill 2048
 ;; have C-u followed by repeated C-SPC keep popping
 set-mark-command-repeat-pop t
 osm-copyright nil
 ;; follow links to version-controlled files without confirming
 vc-follow-symlinks t
 vc-handled-backends '(Git)
 ;; jump from .c to .h files with find-sibling-file
 find-sibling-rules '(("\\([^/]+\\)\\.c\\'" "\\1.h")
                      ("nextstep/Emacs.app/Contents/Resources/\\(.*\\)\\'"
                       "\\1\\'"))
 ;; use ISO format for calendar dates
 calendar-date-style 'iso
 ;; maintain legibility of rendered text in HTML mails
 shr-color-visible-luminance-min 75
 ;; direct Magit to my Git checkouts directory
 magit-repository-directories '(("~/checkouts/" . 1))
 magit-status-sections-hook
 '(magit-insert-status-headers
   magit-insert-merge-log
   magit-insert-rebase-sequence
   magit-insert-am-sequence
   magit-insert-sequencer-sequence
   magit-insert-bisect-output
   magit-insert-bisect-rest
   magit-insert-bisect-log
   magit-insert-untracked-files
   magit-insert-unstaged-changes
   magit-insert-staged-changes
   magit-insert-stashes
   magit-insert-unpushed-to-pushremote
   magit-insert-recent-commits)
 ;; have Dired operations target another visible Dired buffer
 dired-dwim-target t
 ;; use MPV with EMMS
 emms-player-list '(emms-player-mpv)
 eww-auto-rename-buffer 'title
 browse-url-browser-function #'eww-browse-url
 browse-url-generic-program "open"
 global-auto-revert-non-file-buffers t
 auto-revert-verbose nil
 query-about-changed-file t
 ;; show flymake diagnostics as overlays at eol
 ;; flymake-show-diagnostics-at-end-of-line t
 kill-do-not-save-duplicates t
 show-trailing-whitespace t
 read-extended-command-predicate #'command-completion-default-include-p
 completions-format 'horizontal
 completions-group t
 completions-sort nil
 completions-detailed t
 completion-auto-select nil
 completion-styles '(substring partial-completion)
 completion-category-overrides
 '((buffer       (styles                          substring))
   (file         (styles basic partial-completion substring)
                 (sort-function . minibuffer-sort-by-history))
   (cobra-command-line (styles basic substring partial-completion))
   (project-file (styles basic partial-completion substring))
   (recent-file  (styles basic partial-completion substring))
   (command      (sort-function . minibuffer-sort-by-history)))
 completion-auto-help 'visible
 completions-max-height 16
 completion-auto-wrap t
 read-minibuffer-restore-windows nil
 ;; include CWD in shell command prompts
 shell-command-prompt-show-cwd t
 ;; shell-kill-buffer-on-exit t
 shell-dirtrack-verbose nil
 compilation-scroll-output t
 display-time-default-load-average nil
 ;; allow disabling confirming before compilation via local variables foo bar baz
 safe-local-variable-values '((compilation-read-command . nil))
 xref-search-program 'ripgrep
 grep-use-headings t
 sqlformat-command 'pgformatter
 sql-input-ring-file-name (expand-file-name ".sqli-history" user-emacs-directory)
 ;; use relative line numbers
 display-line-numbers-type 'relative
 ;; persist Git commit message history
 savehist-additional-variables '(log-edit-comment-ring)
 eldoc-minor-mode-string nil
 flyspell-mode-line-string nil
 eglot-confirm-server-edits 'diff
 ;; use my custom project-prompting function
 project-prompter #'esy/read-project-by-name
 ;; have common bindings initially hidden in the output of C-h b
 describe-bindings-outline-rules `((match-regexp
                                    .
                                    ,(regexp-opt
                                      '("Key translations"
                                        "Global Bindings:"
                                        "Function key map translations"
                                        "pixel-scroll-precision-mode"
                                        "context-menu-mode"))))
 TeX-view-program-selection '((output-pdf "PDF Tools"))
 TeX-source-correlate-start-server t
 TeX-data-directory "~/.emacs.d/elpaca/builds/auctex/"
 TeX-lisp-directory TeX-data-directory
 TeX-auto-save t
 TeX-parse-self t
 TeX-source-correlate-mode t
 TeX-electric-sub-and-superscript t
 ;; TeX-electric-escape t
 LaTeX-electric-left-right-brace t
 ;; Read remote man pages
 Man-support-remote-systems t
 ;; confirm-nonexistent-file-or-buffer nil
 extended-command-dim-hyphens t
 read-buffer-completion-ignore-case t
 echo-keystrokes-help nil
 checkdoc-verb-check-experimental-flag nil
 prefix-help-command #'help-complete-keys
 help-enable-variable-value-editing t
 ;; pcomplete-termination-string "" See `comint-completion-addsuffix'.
 imenu-flatten t
 imenu-auto-rescan t
 scroll-conservatively 101              ; For `ultra-scroll'
 rotate-windows-change-selected nil
 use-system-tooltips nil
 x-max-tooltip-size '(80 . 12))

(setq-default indent-tabs-mode nil
              display-line-numbers-width 4)

(setq-default eglot-workspace-configuration
              '(:yaml (:schemas (:kubernetes "*"))))

;;; Load my custom theme

(add-to-list 'custom-theme-load-path
             (expand-file-name "theme/" user-emacs-directory))
(load-theme 'esy t)

;;; Add custom code directory to `load-path'

(add-to-list 'load-path (expand-file-name "lisp/" user-emacs-directory))

(unless (eq system-type 'android)
  (add-to-list 'load-path "~/checkouts/esy-publish/")
  (autoload 'esy-publish-setup        "esy-publish"  nil t)
  (autoload 'esy-publish              "esy-publish"  nil t)
  (autoload 'esy-publish-local-server "esy-publish"  nil t)
  (autoload 'esy-publish-create-post  "esy-publish"  nil t)
  (with-eval-after-load 'recentf
    (add-to-list 'recentf-exclude "~/checkouts/esy-publish/local/.*\.html")
    (add-to-list 'recentf-exclude #'file-remote-p)))

(require 'esy-comm)

(add-to-list 'savehist-additional-variables
             'esy-o365-token-refresh-last-time)

(with-eval-after-load 'gnus-sum
  (keymap-unset gnus-summary-mode-map "M-#" t)
  (keymap-unset gnus-summary-mode-map "M-&" t))

;;; Define custom commands

(defvar-keymap transpose-lines-repeat-map
  :doc "Repeat map for \\[transpose-lines]"
  "C-t" #'transpose-lines)

(put 'transpose-lines 'repeat-map 'transpose-lines-repeat-map)

;;; utility commands

(defun esy/emacs-patch (file)
  (interactive "fPatch file: ")
  (require 'emacsbug)
  (delete-other-windows)
  (let ((pbuf (get-buffer-create "*Patch*"))
        (subject nil))
    (with-current-buffer pbuf
      (insert-file-contents file nil nil nil t)
      (setq subject (mail-fetch-field "Subject"))
      (diff-mode))
    (compose-mail report-emacs-bug-address subject)
    (message-goto-body)
    (insert "Tags: patch\n\n\n\n")
    (mml-attach-file file "text/patch" nil "attachment")
    (message-goto-body)
    (forward-line 2)
    (display-buffer pbuf '(display-buffer-in-direction (direction . right)))
    (message-add-action (lambda ()
                          (when-let (window (get-buffer-window pbuf))
                            (quit-window nil window)))
                        'send 'kill)))

;; (defun esy/kill-dwim ()
;;   "When region is active, kill region, otherwise kill last word."
;;   (interactive)
;;   (if (region-active-p)
;;       (let ((beg (region-beginning))
;;             (end (region-end)))
;;         (kill-region beg end))
;;     (let ((end (point)))
;;       (backward-word)
;;       (kill-region (point) end))))

(defun duplicate-line-stay (arg)
  (interactive "p")
  (require 'misc)
  (let ((duplicate-line-final-position 0))
    (duplicate-line arg)))

(defun esy/ttyper ()
  (interactive)
  (require 'eat)
  (let ((default-directory "~")
        (eat-kill-buffer-on-exit t))
    (eat "ttyper -c ~/.config/ttyper/config.toml" t)))

(defun esy/terminal (arg)
  (interactive "P")
  (if (file-remote-p default-directory)
      (let ((process-environment (cons "TERM=xterm-256color" process-environment)))
        (eat "/bin/bash" arg))
    (eat nil arg)))

(defun esy/hut-builds ()
  (interactive)
  (require 'eat)
  (let ((eat-kill-buffer-on-exit nil))
    (eat "hut builds show -f" t)))

(defun esy/pulse-line (&optional _)
  "Pulse current line."
  (interactive)
  (pulse-momentary-highlight-one-line))

(defun esy/seconds-to-date-string (seconds)
  "Decode and display the date corresponding to SECONDS."
  (interactive (list (if (use-region-p)
                         (string-to-number (buffer-substring (region-beginning)
                                                             (region-end)))
                       (read-number "Unix timestamp: " (round (float-time))))))
  (message (format-time-string "%FT%T%z" (seconds-to-time seconds))))

(defun esy/find-init-file (init)
  "Find the Emacs INIT file."
  (interactive (list user-init-file))
  (find-file init))

(defun esy/tmp-dired ()
  "Start Dired in ~/tmp."
  (interactive)
  (dired "~/tmp"))

(with-eval-after-load 'shell
  (keymap-set shell-mode-map "SPC" #'comint-magic-space))

(defvar-local esy/log-buffer-filename nil
  "File in which the current buffer is logged.")

(defun esy/log-buffer ()
  "Save the current buffer under the ~/logs/ directory."
  (interactive)
  (let ((filename (or esy/log-buffer-filename
                      (setq esy/log-buffer-filename
                            (expand-file-name
                             (concat mode-name
                                     "_"
                                     (format-time-string
                                      "%Y%m%d%H%M%S"
                                      (current-time))
                                     ".log")
                             "~/logs")))))
    (save-restriction
      (widen)
      (write-region (point-min)
                    (point-max)
                    filename))))

(defface mode-face-lisp-interaction-mode '((t :background "#fff6d8")) "")

(with-eval-after-load 'mode-face
  (add-to-list 'mode-face-modes 'lisp-interaction-mode))

(with-eval-after-load 'comint
  (keymap-set comint-mode-map "C-c C-q" #'esy/log-buffer)
  (setq comint-completion-addsuffix nil))

(defun esy/recent-log-summary ()
  "Display a summary of my website's most recent access log."
  (interactive)
  (async-shell-command (string-join (list "ssh"
                                          "[email protected]"
                                          (shell-quote-argument
                                           (string-join '("grep -E '\"GET.+\" 2' /var/log/apache2/access.log"
                                                          "tr -d '\"'"
                                                          "tr -d \"'\""
                                                          "cut -f 7,11,12 -d ' '"
                                                          "sort"
                                                          "uniq -c"
                                                          "sort -n")
                                                        " | ")))
                                    " ")
                       "*Recent Log Summary*"))

(defun esy/access-log-summary ()
  "Display a summary of my website's access log."
  (interactive)
  (async-shell-command (string-join (list "ssh"
                                          "[email protected]"
                                          (shell-quote-argument
                                           (string-join '("zcat /var/log/apache2/access.log.*.gz"
                                                          "cat - /var/log/apache2/access.log /var/log/apache2/access.log.1"
                                                          "grep -E '\"GET.+\" 2'"
                                                          "tr -d '\"'"
                                                          "tr -d \"'\""
                                                          "cut -f 7,11,12 -d ' '"
                                                          "sort"
                                                          "uniq -c"
                                                          "sort -n")
                                                        " | ")))
                                    " ")
                       "*Access Log Summary*"))

(defvar esy/clone-history nil)

(defun esy/clone (remote depth)
  (interactive (list (let ((default (thing-at-point 'url)))
                       (read-string (format-prompt "Clone" default)
                                    nil 'esy/clone-history default))
                     (and current-prefix-arg
                          (prefix-numeric-value current-prefix-arg))))
  (let ((dir (expand-file-name
              (file-name-base (car (url-path-and-query
                                    (url-generic-parse-url remote))))
              "~/checkouts")))
    (apply #'call-process
           (append (list "git" nil nil nil "clone")
                   (when depth
                     (list "--depth" (number-to-string depth)))
                   (list remote dir)))
    (dired dir)))

(defun esy/json-path-to-position (pos)
  "Return the JSON path from the document's root to the element at POS.

The path is represented as a list of strings and integers,
corresponding to the object keys and array indices that lead from
the root to the element at POS."
  (named-let loop ((node (treesit-node-at pos)) (acc nil))
    (if-let ((parent (treesit-parent-until
                      node
                      (lambda (n)
                        (member (treesit-node-type n)
                                '("pair" "array"))))))
        (loop parent
              (cons
               (pcase (treesit-node-type parent)
                 ("pair"
                  (treesit-node-text
                   (treesit-node-child (treesit-node-child parent 0) 1) t))
                 ("array"
                  (named-let check ((i 1))
                    (if (< pos (treesit-node-end (treesit-node-child parent i)))
                        (/ (1- i) 2)
                      (check (+ i 2))))))
               acc))
      acc)))

(defun esy/json-path-at-point (point &optional kill)
  "Display the JSON path at POINT.  When KILL is non-nil, kill it too.

Interactively, POINT is point and KILL is the prefix argument."
  (interactive "d\nP")
  (let ((path (mapconcat (lambda (o) (format "%s" o))
                         (esy/json-path-to-position point)
                         ".")))
    (if kill
        (progn (kill-new path) (message "Copied: %s" path))
      (message path))
    path))

(defun esy/dedicate-window (window flag)
  (interactive (list (get-buffer-window) (not current-prefix-arg)))
  (message "Window is %s dedicated to buffer %s."
           (if flag (if (window-dedicated-p) "already" "now") "no longer")
           (buffer-name))
  (set-window-dedicated-p window flag))

;;; Ensure scripts ran with `executable-interpret' are executable

(with-eval-after-load 'executable
  (define-advice executable-interpret (:before (&rest _) ensure-executable)
    (unless (file-exists-p buffer-file-name)
      (basic-save-buffer))
    (executable-make-buffer-file-executable-if-script-p)))

;;; Pulse the line around point after switching windows

(defun esy/pulse-on-window-selection-change (&rest _)
  (pulse-momentary-highlight-one-line))

(add-hook 'window-selection-change-functions
          #'esy/pulse-on-window-selection-change)

;;; Override the default startup message

(define-advice startup-echo-area-message (:override () report-init-time)
  (format "%s started in %s.  Hack away."
          (propertize "Emacs"           'face 'success)
          (propertize (emacs-init-time) 'face 'error  )))

;;; Extend standard programming mode hooks

(defun esy/prog-set-up-capf ()
  (add-hook 'completion-at-point-functions
            #'tags-completion-at-point-function 90 t))

(add-hook 'completion-at-point-functions #'dabbrev-capf)

(dolist (mode '(bug-reference-prog-mode
                display-fill-column-indicator-mode
                display-line-numbers-mode
                flymake-mode
                electric-indent-local-mode
                ;; esy/prog-set-up-capf
                ))
  (add-hook 'prog-mode-hook mode))

;; (add-hook 'lisp-data-mode-hook #'electric-pair-mode)
(add-hook 'lisp-data-mode-hook #'rainbow-delimiters-mode)

;;; Extend standard text mode hooks

(add-hook 'text-mode-hook #'flyspell-mode)

;;; Bind some keys

(keymap-global-set "C-M-i" #'completion-at-point)
(keymap-global-set "C-c c" #'org-capture)
(keymap-global-set "C-c !" #'flymake-go-to-diagnostic)
(keymap-global-set "C-c f" #'recentf-open)
(keymap-global-set "C-c k" 'kubed-prefix-map)
(keymap-global-set "C-c l" #'org-store-link)
(keymap-global-set "C-c a" #'org-agenda)
(keymap-global-set "C-c v" #'esy/terminal)
(keymap-global-set "C-c p" 'sweeprolog-prefix-map)
(keymap-global-set "C-c S" #'scratch-buffer)
(keymap-global-set "C-c m" #'esy/emms-map)
(keymap-global-set "C-c C" #'esy-publish-create-post)
(keymap-global-set "C-c e" 'esy/elpaca-prefix-map)
(keymap-global-set "C-c E" #'elfeed)
(keymap-global-set "C-c T" #'esy/ttyper)
(keymap-global-set "C-c G" #'gnus)
(keymap-global-set "C-c M" #'mastodon)
(keymap-global-set "C-c t" #'esy/tmp-dired)
(keymap-global-set "C-c y" #'esy/search-mailing-list)
(keymap-global-set "C-c F" #'esy/find-init-file)
(keymap-global-set "C-c 1" #'delete-other-windows)
(keymap-global-set "C-c 2" #'split-window-below)
(keymap-global-set "C-c 3" #'split-window-right)
;; (keymap-global-set "<remap> <kill-region>" #'esy/kill-dwim)
(keymap-global-set "<remap> <suspend-frame>" #'zap-up-to-char)
(keymap-global-set "s-n" #'duplicate-line)
(keymap-global-set "s-p" #'duplicate-line-stay)
(keymap-global-set "s-u" #'universal-argument)
(keymap-global-set "s--" #'negative-argument)
(keymap-global-set "M-#" #'dictionary-search)
(keymap-global-set "M-o" #'previous-buffer)
(keymap-global-set "M-O" #'next-buffer)
(keymap-global-set "C-," #'delete-backward-char)
(keymap-global-set "C-;" #'avy-goto-char-timer)
(keymap-global-set "C-s-f" #'toggle-frame-fullscreen)
(keymap-global-set "C-s-l" #'esy/pulse-line)
(keymap-global-set "M-\"" #'insert-pair)
(keymap-global-set "M-[" #'insert-pair)
(keymap-global-set "M-'" #'insert-pair)
(keymap-global-set "M-]" #'up-list)

(keymap-set ctl-x-map "C-b" #'ibuffer)
(keymap-set ctl-x-map "k" #'esy/kill-buffers)
(find-function-setup-keys)

(keymap-set window-prefix-map "p" #'windmove-swap-states-up)
(keymap-set window-prefix-map "n" #'windmove-swap-states-down)
(keymap-set window-prefix-map "a" #'windmove-swap-states-left)
(keymap-set window-prefix-map "e" #'windmove-swap-states-right)
(keymap-set window-prefix-map "d" #'esy/dedicate-window)
(keymap-set window-prefix-map "r" #'rotate-window-layout-clockwise)
(keymap-set window-prefix-map "R" #'rotate-window-layout-counterclockwise)
(keymap-set window-prefix-map "|" #'flip-window-layout-horizontally)
(keymap-set window-prefix-map "-" #'flip-window-layout-vertically)
(keymap-set window-prefix-map "=" #'fit-window-to-buffer)

(dolist (command '(windmove-swap-states-up
                   windmove-swap-states-down
                   windmove-swap-states-left
                   windmove-swap-states-right
                   rotate-window-layout-clockwise
                   rotate-window-layout-counterclockwise
                   flip-window-layout-horizontally
                   flip-window-layout-vertically))
  (put command 'repeat-map 'window-prefix-map))

(keymap-set isearch-mode-map "C-;" #'avy-isearch)

;;; digit arguments with the super modifier
(dotimes (i 10)
  (keymap-global-set (concat "s-" (number-to-string i))
                     #'digit-argument))

;;; unbind some keys
(dolist (key '("s-a" "s-d" "s-e" "s-f" "s-g" "s-h" "s-j" "s-k" "s-l"
               "s-m" "s-o" "s-q" "s-s" "s-t" "s-w" "s-x" "s-y" "s-z"))
  (keymap-global-unset key))

;;; enable some commands
(dolist (command '(set-goal-column
                   narrow-to-region
                   narrow-to-page
                   downcase-region
                   upcase-region
                   list-timers))
  (put command 'disabled nil))

;;; disable some commands

(put 'suspend-frame 'disabled t)
;;; Configure project management commands

(with-eval-after-load 'project
  (alist-set ?\` project-switch-commands '("flymake" nil project-go-to-diagnostic))
  (alist-set ?m project-switch-commands '("magit" nil magit-project-status))
  (define-key project-prefix-map "w" #'project-copy-relative-file-name-as-kill)
  (define-key project-prefix-map "m" #'magit-project-status)

  (defvar esy/project-name-history nil)

  (defvar esy/projects-directory "~/checkouts/")

  (defun esy/read-project-by-name (&optional _)
    "Read a project name and return its root directory.

If no known project matches the selected name, prompt for a
sub-directory of `esy/projects-directory' using the selected name
as the initial input for completion, and return that directory."
    (let* ((name-dir-alist
            (seq-keep (lambda (dir)
                        (when-let ((proj (project-current nil dir)))
                          (cons (project-name proj) dir)))
                      (project-known-project-roots)))
           (current (project-current))
           (default (and current (project-name current)))
           (name (completing-read (format-prompt "Project" default)
                                  name-dir-alist
                                  nil nil nil
                                  'esy/project-name-history
                                  default)))
      (or (alist-get name name-dir-alist nil nil #'string=)
          (let* ((dir (read-directory-name "Project root directory: "
                                           esy/projects-directory
                                           nil t name))
                 (project (project-current nil dir)))
            (when project (project-remember-project project))
            dir))))

  (defun project-copy-relative-file-name-as-kill ()
    (interactive)
    (if-let ((project (project-current))
             (root (expand-file-name (project-root project)))
             (directory-abbrev-alist (cons (cons root "")
                                           directory-abbrev-alist))
             (fn (abbreviate-file-name (or (buffer-file-name)
                                           (expand-file-name default-directory)))))
        (if (string-empty-p fn)
            (user-error "At project root!")
          (progn
            (kill-new fn)
            (message (concat (propertize "Copied file name " 'face 'shadow)
                             (propertize fn 'face 'bold)
                             (propertize " relative to " 'face 'shadow)
                             (propertize (project-name project) 'face 'italic)
                             (propertize " project root directory" 'face 'shadow)))))
      (user-error "Not a project buffer!"))))

;;; Configure SQL connections

(with-eval-after-load 'sql
  (defun esy/update-sql-connection-alist ()
    (interactive)
    (auth-source-forget-all-cached)
    (setq sql-connection-alist (delete nil
                                       (mapcar (lambda (source)
                                                 (pcase (split-string (plist-get source :host)
                                                                      (rx "^"))
                                                   (`(,con ,db ,host)
                                                    (list (intern con)
                                                          (list 'sql-product ''postgres)
                                                          (list 'sql-user  (plist-get source :user))
                                                          (list 'sql-port  (string-to-number (plist-get source :port)))
                                                          (list 'sql-password  (funcall (plist-get source :secret)))
                                                          (list 'sql-server host)
                                                          (list 'sql-database db)))))
                                               (auth-source-search :port 5432 :max 100)))))
  (esy/update-sql-connection-alist)
  (add-hook 'sql-interactive-mode-hook #'toggle-truncate-lines)
  (add-hook 'sql-interactive-mode-hook #'abbrev-mode)
  (define-key sql-mode-map (kbd "C-c C-f") #'sqlformat)
  (define-advice sql-comint-postgres (:around (fun &rest args) use-sql-password)
    (let ((process-environment
           (nconc
            (list (format "PGPASSWORD=%s" sql-password))
            process-environment)))
      (apply fun args))))

;;; Configure Org mode

(with-eval-after-load 'org
  (keymap-unset org-mode-map "C-," t)
  (unless (eq system-type 'android)
    (esy-publish-setup)))

(with-eval-after-load 'org-agenda
  (add-to-list 'org-agenda-custom-commands
               '("w" "Work TODOs" tags-todo "+work"))
  (add-to-list 'org-agenda-custom-commands
               '("h" "Holland TODOs" tags-todo "+holland")))

;;; Configure `dired'

(with-eval-after-load 'dired
  (put 'dired-find-alternate-file 'disabled nil))
;;; Configure remote access via `tramp'

(with-eval-after-load 'tramp
  (tramp-set-completion-function "ssh" '((tramp-parse-netrc "~/.authinfo.gpg")))
  (add-to-list 'backup-directory-alist (cons tramp-file-name-regexp nil)))

;;; Configure `proced'

(with-eval-after-load 'proced
  (add-hook 'proced-mode-hook (lambda ()
                                (setq proced-auto-update-flag t))))

;;; Configure `world-clock'

(with-eval-after-load 'time
  (add-to-list 'zoneinfo-style-world-list '("Europe/Amsterdam" "Amsterdam"))
  (add-to-list 'zoneinfo-style-world-list '("Asia/Tel_Aviv" "Tel Aviv")))

;;; Configure spelling errors correction via `flyspell'
(with-eval-after-load 'flyspell
  (keymap-unset flyspell-mode-map "C-," t)
  (keymap-unset flyspell-mode-map "C-." t)
  (keymap-unset flyspell-mode-map "C-;" t)
  (keymap-unset flyspell-mode-map "C-M-i" t))

;;; Configure minibuffer completions

(add-hook 'completion-list-mode-hook
          (lambda ()
            (setq-local cursor-in-non-selected-windows nil)))

;;; Configure highlighting of the current line via `lin'

(with-eval-after-load 'hl-line
  (dolist (mode '(gnus-summary-mode gnus-group-mode gnus-server-mode))
    (add-to-list 'global-hl-line-modes mode)))

;;; Enable some global minor modes
(column-number-mode)
(context-menu-mode)
(display-battery-mode)
(display-time-mode)
(global-auto-revert-mode)
(global-completion-preview-mode)
(global-diff-hl-mode)
(global-hl-line-mode)
(global-minibuffer-auto-completion-mode)
(kubed-menu-bar-mode)
(minibuffer-depth-indicate-mode)
;; (pixel-scroll-precision-mode
(ultra-scroll-mode)
(recentf-mode)
(repeat-mode)
(save-place-mode)
(savehist-mode)
(transient-mark-mode)
(winner-mode)

;;; Set up EMMS

(dolist (command '(emms-start
                   emms-stop
                   emms-next
                   emms-previous
                   emms-pause
                   emms-seek
                   emms-seek-to
                   emms-seek-forward
                   emms-seek-backward
                   emms-show))
  (autoload command "emms"))

(defvar-keymap esy/emms-map
  :doc "My keymap for EMMS commands."
  :prefix 'esy/emms-map
  :repeat (:hints ((emms-next          . "Next")
                   (emms-previous      . "Previous")
                   (emms-stop          . "Stop")
                   (emms-seek-backward . "backward")
                   (emms-seek-forward  . "forward")
                   (emms-seek          . "seek")
                   (emms-show          . "show")
                   (emms-pause         . "pause")
                   (emms-start         . "start")
                   (emms-seek-to       . "to")))
  "N" #'emms-next
  "P" #'emms-previous
  "S" #'emms-stop
  "b" #'emms-seek-backward
  "f" #'emms-seek-forward
  "k" #'emms-seek
  "m" #'emms-show
  "p" #'emms-pause
  "s" #'emms-start
  "t" #'emms-seek-to)

(with-eval-after-load 'emms
  (emms-minimalistic))

;;; Configure PDF handling

(pdf-loader-install)

(add-to-list 'revert-without-query "\\.pdf\\'")

;;; Configure Prolog integration via `sweeprolog'

(setq-default prolog-system 'swi)
(add-to-list 'major-mode-remap-alist '(prolog-mode . sweeprolog-mode))
;; (setq sweeprolog-swipl-path "/Applications/SWI-Prolog.app/Contents/MacOS/swipl")
(with-eval-after-load 'sweeprolog
  (setq sweeprolog-top-level-persistent-history
        (locate-user-emacs-file ".sweep_history"))
  (add-hook 'sweeprolog-mode-hook #'sweeprolog-electric-layout-mode)
  (add-hook 'sweeprolog-top-level-mode-hook #'compilation-shell-minor-mode))

;;; Configure recursive grepping via `rg'

(with-eval-after-load 'grep
  (add-to-list 'grep-files-aliases '("pl" . "*.pl *.plt *.pro *.prolog")))

;;; Configure custom faces in `terraform-mode'

(with-eval-after-load 'terraform-mode
  (setq terraform--resource-name-face font-lock-function-name-face
        terraform--resource-type-face font-lock-type-face))

;;; Associate major modes with files based on their extensions

(dolist (cell '(("Dockerfile\\'"  . dockerfile-ts-mode)
                ("\\.ya?ml\\'" . yaml-ts-mode)
                ("\\.toml\\'"  . toml-ts-mode)
                ("\\.json\\'"  . json-ts-mode)
                ("\\.tfstate\\'"  . json-ts-mode)
                ("\\.ts\\'"    . typescript-ts-mode)
                ("\\.rb\\'"    . ruby-ts-mode)
                ("\\.rs\\'"    . rust-ts-mode)
                ("\\.go\\'"    . go-ts-mode)
                ("\\.plt?\\'"  . prolog-mode)))
  (push cell auto-mode-alist))

;;; Configure Help

(with-eval-after-load 'help-fns
  (with-eval-after-load 'shortdoc
    (add-hook 'help-fns-describe-function-functions
              #'shortdoc-help-fns-examples-function)))

;;; Configure dictionary

(with-eval-after-load 'dictionary
  (setopt dictionary-search-interface   'help
          dictionary-default-dictionary "gcide"
          dictionary-default-strategy   "prefix"
          dictionary-server             "dict.org"))

;;; Configure news feeds

(with-eval-after-load 'elfeed
  (keymap-set elfeed-show-mode-map "S-SPC" #'scroll-down-command)

  (defvar esy/feeds-file (locate-user-emacs-file "feeds.eld"))

  (defun esy/feeds ()
    (with-temp-buffer
      (insert-file-contents-literally esy/feeds-file)
      (goto-char (point-min))
      (read (current-buffer))))

  (defun esy/read-feed-keywords ()
    (mapcar #'intern
            (completing-read-multiple
             "Keywords: "
             (let ((keywords nil))
               (dolist
                   (feed-keywords
                    (mapcar
                     #'cdr
                     (esy/feeds)))
                 (mapc (lambda (keyword)
                         (add-to-list 'keywords keyword))
                       feed-keywords))
               (mapcar #'symbol-name keywords)))))

  (defun esy/add-feed (url keywords)
    (interactive (let ((default (thing-at-point-url-at-point)))
                   (list (read-string (format-prompt "Feed URL"
                                                     default)
                                      nil nil default)
                         (esy/read-feed-keywords))))
    (let ((feeds (cons (cons url keywords) (esy/feeds))))
      (with-temp-buffer
        (pp (cons (cons url keywords) (esy/feeds))
            (current-buffer))
        (write-region (point-min)
                      (point-max)
                      esy/feeds-file))
      (setq elfeed-feeds feeds)))

  (defun esy/update-feeds ()
    (interactive)
    (setq elfeed-feeds (esy/feeds)))

  (with-eval-after-load 'eww
    (defun esy/eww-add-feed (url keywords)
      (interactive (list (eww-read-alternate-url)
                         (esy/read-feed-keywords))
                   eww-mode)
      (esy/add-feed url keywords)))

  (setq
   ;; read feeds list from a separate file
   elfeed-feeds (esy/feeds))

  (with-eval-after-load 'completion-preview
    (push '(not elfeed-search-mode elfeed-show-mode)
          global-completion-preview-modes)))

;;; Disable some minor-mode mode-line lighters

(dolist (mm '((whitespace-cleanup-mode . whitespace-cleanup-mode)
              (outline                 . outline-minor-mode)
              (abbrev                  . abbrev-mode)
              (iimage                  . iimage-mode)))
  (with-eval-after-load (car mm)
    (setf (alist-get (cdr mm) minor-mode-alist) '(""))))

;;; Configure Texinfo mode

(with-eval-after-load 'texinfo
  (add-hook 'texinfo-mode-hook #'abbrev-mode))

;;; Configure TeX

(add-to-list 'major-mode-remap-alist '(latex-mode . Latex-Mode))

(with-eval-after-load 'tex-mode
  (require 'tex))

(with-eval-after-load 'tex
  (setopt TeX-modes '(tex-mode plain-tex-mode latex-mode doctex-mode))
  (add-hook 'plain-TeX-mode-hook
            (lambda () (set (make-local-variable 'TeX-electric-math)
                            (cons "$" "$"))))
  (add-hook 'TeX-after-compilation-finished-functions #'TeX-revert-document-buffer))

(with-eval-after-load 'latex
  (add-hook 'LaTeX-mode-hook
            (lambda () (set (make-local-variable 'TeX-electric-math)
                            (cons "\\(" "\\)"))))

  (add-hook 'LaTeX-mode-hook #'LaTeX-math-mode)

  (defun LaTeX-mark-math ()
    (interactive)
    (when-let ((beg (search-backward "\\(" nil t))
               (end (search-forward "\\)" nil t)))
      (set-mark beg)))

  (keymap-set LaTeX-mode-map "C-c C-SPC" #'LaTeX-mark-math))

(with-eval-after-load 'elisp-mode
  (setq elisp-flymake-byte-compile-load-path (cons "./" load-path))
  (keymap-set emacs-lisp-mode-map "C-c C-t" #'trace-function)
  (keymap-set emacs-lisp-mode-map "C-c C-s" #'set-variable)
  (add-hook 'emacs-lisp-mode-hook #'cursor-sensor-mode))

(dolist (mm '((go-ts-mode         . go-ts-mode-hook)
              (typescript-ts-mode . typescript-ts-mode-hook)
              (python             . python-base-mode-hook)
              (cc-mode            . c-mode-hook)))
  (with-eval-after-load (car mm) (add-hook (cdr mm) #'eglot-ensure)))

(with-eval-after-load 'completion-preview
  (add-to-list 'completion-preview-commands 'org-self-insert-command)
  (add-to-list 'completion-preview-commands #'lisp-delete-backward)
  (setq completion-preview-minimum-symbol-length 2)
  (keymap-set completion-preview-active-mode-map "M-n" #'completion-preview-next-candidate)
  (keymap-set completion-preview-active-mode-map "M-p" #'completion-preview-prev-candidate)
  (keymap-set completion-preview-active-mode-map "M-f" #'completion-preview-insert-word))

(add-hook 'read-shell-command-minibuffer-setup-hook #'completion-preview-mode)
(add-hook 'eval-expression-minibuffer-setup-hook #'completion-preview-mode)
(add-hook 'eval-expression-minibuffer-setup-hook #'electric-pair-mode)

;; (load-file "/usr/local/opt/agda/libexec/ghc-9.10.1-inplace/Agd-2.6.4.3-7d670049/share/emacs-mode/agda2.el")

(defun read-buffer-to-switch-recentf (file)
  (interactive
   (minibuffer-with-setup-hook #'minibuffer-completion-help
     (unless recentf-mode (recentf-mode 1))
     (list (completing-read (format-prompt "Open recent file" nil)
                            recentf-list nil t (minibuffer-contents)))))
  (let ((buf (find-file-noselect file)))
    (delete-minibuffer-contents)
    (insert (buffer-name buf))
    (exit-minibuffer)))

(defun read-buffer-to-switch-bookmark (bookmark)
  (interactive
   (minibuffer-with-setup-hook #'minibuffer-completion-help
     (list (bookmark-completing-read "Jump to bookmark"))))
  (let ((buf (save-current-buffer
               (bookmark-handle-bookmark bookmark)
               (current-buffer))))
    (delete-minibuffer-contents)
    (insert (buffer-name buf))
    (exit-minibuffer)))

;; (defun read-buffer-isearch (buffers string)
;;   (interactive
;;    (let ((s (minibuffer-contents-no-properties)))
;;      (list (remove (current-buffer)
;;                    (seq-filter
;;                     (lambda (b)
;;                       (and (buffer-match-p '(not (or (major-mode . messages-buffer-mode)
;;                                                      (major-mode . fundamental-mode)))
;;                                            b)
;;                            (with-current-buffer b
;;                              (save-excursion
;;                                (goto-char (point-min))
;;                                (search-forward s nil t)))))
;;                     (buffer-list)))
;;            s))
;;    minibuffer-mode)
;;   (unless buffers
;;     (user-error "No buffers contain `%s'" string))
;;   (multi-isearch-buffers buffers)
;;   (isearch-process-search-string string string))

(defvar-keymap read-buffer-to-switch-mode-map
  :doc "Keymap for `read-buffer-to-switch-mode'."
  "M-m" #'read-buffer-to-switch-bookmark
  "M-r" #'read-buffer-to-switch-recentf
  ;; "C-s" #'read-buffer-isearch
  )

(define-minor-mode read-buffer-to-switch-mode
  "Minor mode for reading a buffer name in the minibuffer."
  :group 'minibuffer)

(defun esy/read-buffer (prompt &optional def require-match predicate)
  (minibuffer-with-setup-hook
      (:append
       (lambda ()
         (when read-buffer-to-switch-current-buffer
           (read-buffer-to-switch-mode))))
    (let ((read-buffer-function nil))
      (read-buffer prompt def require-match predicate))))

(defun esy/kill-buffers (buffers)
  (interactive
   (list (let ((def (buffer-name)))
           (minibuffer-with-setup-hook
               (lambda () (setq minibuffer-action
                                (cons (lambda (buf-name)
                                        (kill-buffer buf-name)
                                        (message "Killed %s" buf-name))
                                      "kill")
                                minibuffer-alternative-action
                                (cons #'display-buffer "display")))
             (completing-read-multiple
              (format-prompt "Kill buffers" def)
              (completion-buffer-name-table) nil t nil
              'buffer-name-history def)))))
  (when buffers
    (let ((num (length buffers)))
      (while (and buffers (kill-buffer (car buffers)))
        (setq buffers (cdr buffers)))
      (let* ((spared (length buffers))
             (killed (- num spared)))
        (message "Killed %d buffer%s%s."
                 killed (ngettext "" "s" killed)
                 (if buffers (format ", %s spared" spared) ""))))))

(setq read-buffer-function #'esy/read-buffer)

(load-file (expand-file-name "admin/cherry.el" source-directory))

(with-eval-after-load 'magit
  (with-eval-after-load 'diff-hl
    (add-hook 'magit-pre-refresh-hook  'diff-hl-magit-pre-refresh)
    (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh)))

(with-eval-after-load 'xref
  (add-hook 'xref-after-update-hook #'outline-minor-mode))

(defun esy/search-mailing-list (q)
  (interactive "sSearch: ")
  (eww (url-encode-url (format "yhetil.org/emacs?q=%s" q))))

(with-eval-after-load 'vc-git
  (add-hook 'vc-git-region-history-mode-hook #'bug-reference-mode))

(add-hook 'find-function-after-hook #'esy/pulse-line)

(defun esy/update-forks ()
  (interactive)
  (require 'with-editor)
  (require 'git-commit)
  (require 'server)
  (unless (server-running-p) (server-start))
  (with-editor
    (compile (string-join
              (mapcar (lambda (name)
                        (format
                         "echo Updating: %s && git -C %s pull && git -C %s push"
                         name name name))
                      (mapcar (lambda (base)
                                (expand-file-name base "~/checkouts"))
                              '("auctex" "pdf-tools" "magit" "tablist")))
              " && "))))

(defvar esy/trusted-files `(,user-init-file ,early-init-file))

(setq trusted-content
      (mapcar #'abbreviate-file-name
              (append esy/trusted-files
                      (mapcar #'file-name-as-directory load-path))))

(provide 'init)
;;; init.el ends here

Mail and other communication settings

;;; esy-comm.el --- My communication settings       -*- lexical-binding: t; -*-

;; Copyright (C) 2023  Eshel Yaron

;; Author: Eshel Yaron <[email protected]>
;; Keywords: comm

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;;

;;; Code:

(require 'esy-o365)

(defvar esy-comm-accounts
  '(("me"
     "[email protected]" "mail.eshelyaron.com" "mail.eshelyaron.com")
    ("gmail"
     "[email protected]" "imap.gmail.com" "smtp.gmail.com")
    ;; ("swi"
    ;;  "[email protected]"
    ;;  ;; "mx.transip.email" "mx.transip.email"
    ;;  ;; "mail.swi-prolog.com" "mail.swi-prolog.com"
    ;;  )
    ("uva"
     "[email protected]" "outlook.office365.com" "smtp.office365.com"
     (nnimap-authenticator xoauth2))))

(defvar esy-comm-summary-line-format
  (concat "%1{%U%R%z%}%B"
          "%2{%~(form (replace-regexp-in-string \" via .*\" \"\" (gnus-summary-from-or-to-or-newsgroups gnus-tmp-header gnus-tmp-from)))@%} "
          "%3{(%&user-date;, %k)%}:%* %s\n"))

(setq
 ;; Message
 message-alternative-emails (regexp-opt (mapcar #'cadr esy-comm-accounts))
 message-dont-reply-to-names message-alternative-emails
 message-elide-ellipsis "\n[...%l lines elided...]\n"
 message-generate-hashcash nil
 message-server-alist (mapcar (pcase-lambda
                                (`(,_ ,cond ,_ ,server . ,_))
                                (cons cond
                                      (format "smtp %s 587" server)))
                              esy-comm-accounts)
 send-mail-function #'message-multi-smtp-send-mail

 ;; Gnus
 gnus-expert-user t
 gnus-always-read-dribble-file t
 gnus-break-pages nil
 gnus-inhibit-startup-message t
 gnus-cite-parse-max-size nil
 gnus-face-1 'bold
 gnus-face-2 'gnus-cite-1
 gnus-face-3 'shadow
 gnus-summary-line-format esy-comm-summary-line-format
 gnus-simplify-subject-functions '(gnus-simplify-subject-re)
 gnus-treat-display-smileys nil
 gnus-select-method '(nntp "news.gmane.io")
 gnus-secondary-select-methods (cons
                                '(nntp "news.eternal-september.org")
                                (mapcar
                                 (pcase-lambda
                                   (`(,name ,_ ,address ,_ . ,tail))
                                   `(nnimap ,name
                                            (nnimap-address ,address)
                                            (nnimap-server-port "imaps")
                                            (nnimap-stream ssl)
                                            .
                                            ,tail))
                                 esy-comm-accounts))
 gnus-no-groups-message "No new articles"
 gnus-use-full-window nil
 gnus-article-treat-types '("text/plain"
                            "text/x-verbatim"
                            "text/x-patch"
                            "text/html"
                            "text/calendar")
 gnus-icalendar-org-capture-file "~/org/inbox.org"
 gnus-icalendar-org-capture-headline '("Calendar")
 gnus-large-newsgroup 2048
 nntp-connection-timeout 15

 ;; Mastodon
 mastodon-instance-url "https://social.eshelyaron.com"
 mastodon-active-user "eshel"

 ;; BBDB
 bbdb-phone-style nil
 bbdb-complete-mail nil
 bbdb-complete-mail-allow-cycling t
 bbdb-completion-display-record nil

 ;; emacsbug.el
 report-emacs-bug-no-explanations t
 submit-emacs-patch-display-help nil

 ;; rcirc.el
 rcirc-default-nick "esy"
 rcirc-server-alist '(("irc.libera.chat"
                       :channels ("#emacs")
                       :port 6697
                       :encryption tls))
 rcirc-log-flag t)

(with-eval-after-load 'gnus
  (require 'gnus-icalendar)
  (add-hook 'gnus-group-mode-hook #'gnus-topic-mode)
  (unless (eq system-type 'android)
    (esy-o365-setup 'gnus))
  (gnus-icalendar-setup)
  (gnus-icalendar-org-setup)
  (bbdb-initialize 'gnus 'mail 'message))

(with-eval-after-load 'message
  (add-hook 'message-send-hook 'ispell-message))

(provide 'esy-comm)
;;; esy-comm.el ends here

Custom libraries

Refresh OAuth 2.0 access token JIT for connecting to my university mail

;;; esy-o365.el --- Settings for outlook 365       -*- lexical-binding: t; -*-

;; Copyright (C) 2023  Eshel Yaron

;; Author: Eshel Yaron <[email protected]>
;; Keywords: comm

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;;

;;; Code:

(defvar esy-o365-token-directory "~/checkouts/M365-IMAP/")
(defvar esy-o365-token-refresh-last-time 0)
(defvar esy-o365-token-refresh-command '("python3" "refresh_token.py"))

(defun esy-o365-refresh-token ()
  (interactive)
  (files--ensure-directory esy-o365-token-directory)
  (when-let ((default-directory esy-o365-token-directory)
             (token (car (process-lines "python3" "refresh_token.py"))))
    (with-temp-buffer
      (insert "machine smtp.office365.com smtp-auth xoauth2 login [email protected] port 587 password \""
              token
              "\"\nmachine outlook.office365.com login [email protected] port imaps password \""
              token
              "\"\n")
      (write-region (point-min) (point-max) ".authinfo"))
    (auth-source-forget-all-cached)))

(defun esy-o365-maybe-refresh-token ()
  (let ((now (float-time)))
    (when (< (* 60 45) (- now esy-o365-token-refresh-last-time))
      (esy-o365-refresh-token)
      (setq esy-o365-token-refresh-last-time now))))

;;;###autoload
(defun esy-o365-setup (mua)
  (require 'auth-source)
  (if (not (file-exists-p esy-o365-token-directory))
      (let ((m "`esy-o365-token-directory' does not exist"))
        (if after-init-time (user-error m) (display-warning 'comm m)))
    (require 'auth-source)
    (add-to-list 'auth-sources
                 (expand-file-name ".authinfo" esy-o365-token-directory))
    (pcase mua
      ('gnus
       (dolist (hook '(gnus-before-startup-hook
                       gnus-before-resume-hook
                       gnus-select-group-hook
                       gnus-get-new-news-hook))
         (add-hook hook #'esy-o365-maybe-refresh-token))))))

(provide 'esy-o365)
;;; esy-o365.el ends here

Jump to any button in the current buffer


Automatically update the completions buffer


My custom theme

;;; esy-theme.el --- My custom theme -*- lexical-binding:t -*-

;; Copyright (C) 2023 Eshel Yaron

;; This file is NOT part of GNU Emacs.

;;; Commentary:

;;; Code:

(deftheme esy "My custom theme.")

(custom-theme-set-faces
 'esy
 `(default ((t . ,(append (when (eq system-type 'darwin) '(:height 130))
                          (unless (eq system-type 'android) '(:family "Iosevka"))))))
 `(fixed-pitch ((t . ,(unless (eq system-type 'android) '(:family "Iosevka")))))
 `(variable-pitch ((t . ,(unless (eq system-type 'android) '(:family "Iosevka Etoile")))))
 '(mode-line ((t)))
 '(mode-line-active ((t :overline "black" :background "lavender")))
 '(mode-line-inactive ((t :underline "black")))
 '(fringe ((t)))
 '(line-number-current-line ((t :background "yellow" :inherit default)))
 '(fill-column-indicator ((t :foreground "orange" :weight light)))
 '(vertical-border ((t :foreground "grey")))
 '(minibuffer-prompt ((t :weight bold :slant italic :foreground "red")))
 '(italic ((t :slant italic)))
 '(cursor ((t :background "salmon")))
 '(font-lock-doc-face ((t :inherit font-lock-string-face :slant italic)))
 ;; '(font-lock-variable-name-face ((t :foreground "#8b0a50")))
 ;; '(font-lock-variable-use-face ((t :foreground "sienna")))
 '(elisp-shorthand-font-lock-face ((t :inherit font-lock-keyword-face :slant italic)))
 '(elfeed-search-title-face ((t :inherit shadow)))
 '(tooltip ((t :background "#f0fff0")))
 ;; '(error ((t :foreground "Red1")))
 )

(custom-theme-set-variables
 'esy
 '(mode-line-position-column-line-format
   '("%5l" (4 "|%c")))
 '(mode-line-position
   '((line-number-mode
      (column-number-mode
       mode-line-position-column-line-format
       mode-line-position-line-format)
      (column-number-mode
       mode-line-position-column-format))
     " " (8 ("" mode-line-percent-position "/%I"))))
 '(mode-line-format
   '(" %+ "
     (:eval (when-let (project
                       (and (not (file-remote-p default-directory))
                            (project-current)))
              (list
               (propertize (concat (project-name project) "/ ")
                           'face 'italic))))
     (:propertize "%b" face mode-line-buffer-id)
     " (%["
     mode-name mode-line-process minor-mode-alist
     (:eval (when (window-dedicated-p)
              " Dedicated"))
     "%n%])"
     (vc-mode vc-mode)
     (which-func-mode (" " which-func-format))
     mode-line-format-right-align
     (:eval (when (mode-line-window-selected-p)
              (list "" 'display-time-string 'battery-mode-line-string)))
     mode-line-position "  "))
 '(elfeed-search-face-alist '((unread default)))
 '(mode-face-faces '(default fringe mode-line-inactive))
 '(mode-face-global-mode t))

(provide-theme 'esy)
;;; esy-theme.el ends here