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


;;; Install packages

(elpaca org-transclusion)
(elpaca (sweeprolog
         :files ("*.org" "*.texi" "sweep.pl"
                 "sweeprolog-pce-theme.el" "sweeprolog.el")))
(elpaca avy)
(elpaca (bbdb
         :repo "https://git.savannah.nongnu.org/git/bbdb.git"
         :files (:defaults "lisp/*.el.in")
         :pre-build (("./autogen.sh")
                     ("./configure")
                     ("make"))))
(elpaca (breadcrumb :repo "https://github.com/joaotavora/breadcrumb"))
(elpaca (corfu
         :pre-build (("mv" "extensions/corfu-indexed.el" "corfu-indexed.el")
                     ("rm" "-r" "extensions")
                     ("emacs" "--batch" "-l" "ox-texinfo" "README.org"
                      "--eval" "(setq org-babel-confirm-evaluate-answer-no t)"
                      "-f" "org-texinfo-export-to-texinfo"))))
(elpaca debbugs)
(elpaca (devdocs
         :pre-build (("emacs" "--batch" "-l" "ox-texinfo" "README.org"
                      "--eval" "(setq org-babel-confirm-evaluate-answer-no t)"
                      "-f" "org-texinfo-export-to-texinfo")
                     ("mv" "README.texi" "devdocs.texi"))))
(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 embark-consult)
(elpaca emms)
(elpaca gnu-elpa-keyring-update)
(elpaca htmlize)
(elpaca keycast)
(elpaca kubernetes)
(elpaca (oauth2
         :repo "git://git.sv.gnu.org/emacs/elpa"
         :local-repo "oauth2"
         :branch "externals/oauth2"))
(elpaca (lin
         :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 magit)
(elpaca (markdown-mode
         :pre-build (("pandoc" "-o" "markdown-mode.texi" "README.md"))))
(elpaca mastodon)
(elpaca orderless)
(elpaca (marginalia
         :pre-build (("emacs" "--batch" "-l" "ox-texinfo" "README.org"
                      "--eval" "(setq org-babel-confirm-evaluate-answer-no t)"
                      "-f" "org-texinfo-export-to-texinfo"))
         :repo "https://github.com/minad/marginalia.git"))
(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 (openai :repo "https://git.sr.ht/~eshel/openai.el"))
(elpaca package-lint)
(elpaca paredit)
(elpaca rainbow-delimiters)
(elpaca rainbow-mode)
(elpaca rg)
(elpaca sqlformat)
(elpaca (mode-face :repo "https://git.sr.ht/~eshel/mode-face"))
(elpaca terraform-mode)
(elpaca whitespace-cleanup-mode)
(elpaca rfc-mode)
(elpaca (auctex
         :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 (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 cape)

(elpaca-wait)

(defvar-keymap esy/elpaca-prefix-map
  :doc "Keymap for `elpaca' commands."
  "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
  "i" #'elpaca-info
  "t" #'elpaca-try)

(defalias 'esy/elpaca-prefix-map esy/elpaca-prefix-map)

;;; 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)
 ;; silence native compilation warnings
 native-comp-async-report-warnings-errors 'silent
 ;; 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
 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)
                 (: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-mhe
               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
 ;; increase maximum number of recent files Emacs remembers
 recentf-max-saved-items 128
 ;; 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
 ;; jump from .c to .h files with find-sibling-file
 find-sibling-rules '(("\\([^/]+\\)\\.c\\'" "\\1.h"))
 ;; 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))
 ;; 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 'one-column
 completion-auto-select nil
 completion-styles '(orderless partial-completion basic)
 completion-show-help nil
 ;; completions-header-format nil
 ;; completion-auto-help 'visible
 completions-max-height 16
 completion-auto-wrap t
 completion-at-point-functions nil
 corfu-cycle t
 corfu-indexed-start     1
 shell-kill-buffer-on-exit t
 compilation-scroll-output t
 display-time-default-load-average nil
 ;; allow disabling confirming before compilation via local variables
 safe-local-variable-values '((compilation-read-command . nil))
 xref-show-definitions-function #'consult-xref
 xref-show-xrefs-function       #'consult-xref
 xref-search-program             'ripgrep
 ;; include CWD in shell command prompts
 shell-command-prompt-show-cwd 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
 paredit-lighter 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)

(setq-default indent-tabs-mode nil)

;;; 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))
(autoload 'some-button "some-button" nil t)
(autoload 'completions-auto-update-mode "completions-auto-update" nil t)

(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")))

(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))

;;; 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))))

(defvar duplicate-line-final-position)

(defun duplicate-line-stay (arg)
  (interactive "p")
  (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)
  (add-hook 'comint-mode-hook #'completion-preview-mode))

(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)
  (let ((default-directory "/ssh:[email protected]:/var/log/apache2/"))
    (async-shell-command (string-join  '("zcat access.log.*.gz"
                                         "cat - access.log 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/transcribe ()
  (interactive)
  (message "Recording...")
  (let ((process
         (start-process "ffmpeg" nil "ffmpeg"
                        "-f" "avfoundation"
                        "-i" "1:0"
                        "-ar" "16000"
                        "-y"
                        "/tmp/foo.wav")))
    (set-transient-map
     (make-sparse-keymap) nil
     (lambda ()
       (message "Stopping recording")
       (interrupt-process process)
       (accept-process-output process 1 nil t)
       (message "Transcribing...")
       (message
        (string-trim-right
         (string-trim-left
          (with-temp-buffer
            (call-process "whisper"
                          nil '(t nil) nil
                          "-m"
                          "/Users/eshelyaron/checkouts/whisper.cpp/models/ggml-base.en.bin"
                          "--no-timestamps" "-f" "/tmp/foo.wav")
            (goto-char (point-min))
            (while (search-forward "[BLANK_AUDIO]" nil t)
              (replace-match "" nil t))
            (buffer-string))))))
     "Recording...  Press any key to stop")))

(defun esy/record (timeout)
  (interactive "p")
  (message "Recording...")
  (call-process "ffmpeg"
                nil nil nil
                "-f" "avfoundation"
                "-i" "1:0" "-t"
                (number-to-string (max timeout 2))
                "-ar" "16000" "-y" "/tmp/foo.wav")
  (openai-chat (string-trim-right
                (string-trim-left
                 (with-temp-buffer
                   (call-process "whisper"
                                 nil '(t nil) nil
                                 "-m"
                                 "/Users/eshelyaron/checkouts/whisper.cpp/models/ggml-base.en.bin"
                                 "--no-timestamps" "-f" "/tmp/foo.wav")
                   (goto-char (point-min))
                   (while (search-forward "[BLANK_AUDIO]" nil t)
                     (replace-match "" nil t))
                   (buffer-string))))))

(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))

(defun esy/bump (tag-prefix)
  (interactive (list "v"))
  (require 'lisp-mnt)
  (require 'magit-apply)
  (let ((date (format-time-string "%F" (current-time)))
        (current-version (save-excursion (lm-header "package-version"))))
    (unless current-version
      (user-error "No Package-Version Elisp header found"))
    (let ((next-version (mapconcat #'number-to-string
                                   (pcase (version-to-list
                                           current-version)
                                     (`(,major ,minor ,patch)
                                      (list major (1+ minor) patch)))
                                   ".")))
      (with-current-buffer (find-file "NEWS.org")
        (goto-char (point-min))
        (re-search-forward (concat "^\\(" org-outline-regexp "\\)") nil t)
        (beginning-of-line)
        (if (looking-at (rx "* Version "
                            (group-n 1
                              (+ digit) "."
                              (+ digit) "."
                              (+ digit))
                            " in development"))
            (progn
              (setq next-version (match-string-no-properties 1))
              (end-of-line)
              (delete-char -14)
              (insert "on " date)
              (forward-char 2))
          (insert "* Version " next-version " on " date "\n\n")))
      (lm-header "package-version")
      (delete-region (point) (pos-eol))
      (insert next-version)
      (vc-print-root-log)
      (find-file-other-window "NEWS.org")
      (named-let loop ()
        (recursive-edit)
        (unless (y-or-n-p "OK to stage, commit and tag the new version?")
          (loop)))
      (unless (= (call-process "git" nil nil nil
                               "add" (buffer-file-name) "NEWS.org")
                 0)
        (error "Git add failed"))
      (unless (= (call-process "git" nil nil nil
                               "commit" "-m"
                               (concat "Announce recent changes "
                                       "in NEWS.org "
                                       "and bump version to "
                                       next-version))
                 0)
        (error "Git commit failed"))
      (unless (= (call-process "git" nil nil nil
                               "tag" "-s" "-m"
                               (concat "Release version "
                                       next-version)
                               (concat tag-prefix next-version))
                 0)
        (error "Git tag failed")))))

;;; 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

(dolist (mode '(bug-reference-prog-mode
                display-fill-column-indicator-mode
                display-line-numbers-mode
                flymake-mode
                ;; flyspell-prog-mode
                completion-preview-mode
                ))
  (add-hook 'prog-mode-hook mode))

(add-hook 'lisp-data-mode-hook #'paredit-mode)
(add-hook 'lisp-data-mode-hook (lambda ()
                                 (when (require 'rainbow-delimiters nil t)
                                   (rainbow-delimiters-mode))))

;;; Extend standard text mode hooks

(add-hook 'text-mode-hook #'flyspell-mode)
(add-hook 'text-mode-hook #'completion-preview-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 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 !" #'consult-flymake)
(keymap-global-set "C-c C" #'esy-publish-create-post)
(keymap-global-set "C-c O" #'openai-chat)
(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 R" #'esy/record)
(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 F" #'esy/find-init-file)
(keymap-global-set "C-c SPC" #'consult-mark)
(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> <goto-line>" #'consult-goto-line)
(keymap-global-set "<remap> <suspend-frame>" #'zap-up-to-char)
(keymap-global-set "<remap> <imenu>" #'consult-imenu)
(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-." #'embark-act)
(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 "b" #'consult-buffer)
(keymap-set ctl-x-4-map "b" #'consult-buffer-other-window)
(keymap-set ctl-x-5-map "b" #'consult-buffer-other-frame)
(keymap-set ctl-x-map "r b" #'consult-bookmark)
(keymap-set ctl-x-map "C-b" #'ibuffer)

(keymap-set search-map "r" #'rg)
(keymap-set search-map "l" #'rg-literal)
(keymap-set search-map "b" #'some-button)

(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)

(dolist (command '(windmove-swap-states-up
                   windmove-swap-states-down
                   windmove-swap-states-left
                   windmove-swap-states-right))
  (put command 'repeat-map 'window-prefix-map))

;;; 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))
  (put command 'disabled nil))

;;; disable some commands

(put 'suspend-frame 'disabled t)

;;; Configure project management commands

(with-eval-after-load 'project
  (define-advice project--find-in-directory (:override (dir) no-remote-projects)
    (unless (file-remote-p dir)
      (run-hook-with-args-until-success 'project-find-functions dir)))
  (add-to-list 'project-switch-commands '(project-compile "Compile"))
  (add-to-list 'project-switch-commands '(rg-project "rg"))
  (add-to-list 'project-switch-commands '(magit-project-status "Magit"))
  (add-to-list 'project-switch-commands '(project-shell "Shell"))
  (when (boundp 'project-prefix-map)
    (define-key project-prefix-map "R" #'rg-project)
    (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 ()
    "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
            (delete
             nil
             (mapcar (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 10)))))
  (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))
  (define-advice completion-file-name-table (:filter-args (args) no-remote-file-name-completion)
    (let ((string (car args))
          (pred (cadr args))
          (action (caddr args)))
      (if (and (file-remote-p string) (eq pred #'file-exists-p))
          (list string nil action)
        (list string pred action)))))

;;; 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

(defvar esy/completing-read-commands nil)

(add-to-list 'savehist-additional-variables
             'esy/completing-read-commands)

(define-advice completing-read (:before (&rest _) record-command)
  (cl-incf (alist-get this-command esy/completing-read-commands 0)))

(add-to-list 'display-buffer-alist
             '("\\*Completions\\*"
               (display-buffer-reuse-window display-buffer-at-bottom)
               (window-parameters (mode-line-format . none))))

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

(dolist (key-binding '(("C-p" . minibuffer-previous-completion)
                       ("C-n" . minibuffer-next-completion)
                       ("M-j" . minibuffer-force-complete-and-exit)
                       ("SPC" . nil)))
  (keymap-set minibuffer-local-completion-map
              (car key-binding)
              (cdr key-binding)))

(with-eval-after-load 'consult
  (with-eval-after-load 'embark
    (require 'embark-consult)))

;;; Define some custom `completion-at-point-functions'

(defun file-capf ()
  "File completion at point function."
  (pcase (bounds-of-thing-at-point 'filename)
    (`(,beg . ,end)
     (list beg end #'completion-file-name-table
           :annotation-function (lambda (_) " File")
           :exclusive 'no))))

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

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

(with-eval-after-load 'lin
  (add-to-list 'lin-mode-hooks 'gnus-summary-mode-hook)
  (add-to-list 'lin-mode-hooks 'gnus-group-mode-hook)
  (add-to-list 'lin-mode-hooks 'gnus-server-mode-hook))

;;; Configure `completion-in-region' UI with `corfu'

(with-eval-after-load 'corfu
  (defun esy/margin-formatter (metadata)
    "Format METADATA for `corfu-margin-formatters'."
    (pcase (cdr (assoc 'category metadata))
      ('dabbrev (lambda (_) "… "))))
  (add-to-list 'corfu-margin-formatters #'esy/margin-formatter)
  (corfu-indexed-mode))

;;; Enable some global minor modes

(dolist (mode '(
                column-number-mode
                context-menu-mode
                display-time-mode
                display-battery-mode
                global-auto-revert-mode
                global-diff-hl-mode
                global-whitespace-cleanup-mode
                lin-global-mode
                marginalia-mode
                minibuffer-depth-indicate-mode
                pixel-scroll-precision-mode
                recentf-mode
                repeat-mode
                save-place-mode
                savehist-mode
                show-paren-mode
                transient-mark-mode
                winner-mode
                completions-auto-update-mode
                global-corfu-mode
                mode-face-global-mode
                ))
  (funcall 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 t
  "N" #'emms-next
  "P" #'emms-previous
  "S" #'emms-stop
  "b" #'emms-seek-backward
  "f" #'emms-seek-fofrward
  "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\\'")

;;; Remove some over-intrusive keybindings in `paredit'

(with-eval-after-load 'paredit
  (keymap-unset paredit-mode-map "M-s" t)
  (keymap-unset paredit-mode-map "M-?" t)
  (with-eval-after-load 'eldoc
    (eldoc-add-command 'paredit-RET)))

;;; Configure Prolog integration via `sweeprolog'

(setq-default prolog-system 'swi)
(add-to-list 'major-mode-remap-alist '(prolog-mode . sweeprolog-mode))
(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 'rg
  (add-to-list 'rg-custom-type-aliases '("Prolog" . "*.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)
                ("\\.tex\\'"   . TeX-latex-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)))

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

(dolist (mm '((whitespace-cleanup-mode . whitespace-cleanup-mode)
              (outline                 . outline-minor-mode)
              (abbrev                  . abbrev-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

(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)))

(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
  (push 'org-self-insert-command completion-preview-commands)
  (push 'paredit-backward-delete completion-preview-commands)
  (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-i" #'completion-preview-insert))

(load-file "/Users/eshelyaron/checkouts/agda/src/data/emacs-mode/agda2.el")

(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]" "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-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")

 ;; Mastodon
 mastodon-instance-url "https://emacs.ch"
 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

;;; some-button.el --- Push some button -*- lexical-binding: t -*-

;; Copyright (C) 2021-2023 Eshel Yaron

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

;; This file 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 file 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 file.  If not, see <http://www.gnu.org/licenses/>.

;;; Package-Version: 0.1.0
;;; Package-Requires: ((emacs "29"))

;;; Commentary:

;;; Code:

(defun some-button--completion-candidate (index button)
  "Return a completion candidate for BUTTON prefixed by INDEX."
  (cons (concat (propertize (format "%d " index)
                            'invisible t)
                (truncate-string-to-width
                 (to-line (button-label button))
                 64 nil ?\s t))
        (button-start button)))

(defun some-button--buttons (&optional buffer pom)
  "Return an alist of buttons in BUFFER.
Buttons following POM appear first in the resulting list."
  (with-current-buffer (or buffer (current-buffer))
    (let ((pos (or pom (point)))
          (button (next-button (point-min)))
          (index 1)
          (buttons-before nil)
          (buttons-after nil))
      (while (and button (< (button-start button) pos))
        (setq buttons-before
              (cons (some-button--completion-candidate index button)
                    buttons-before))
        (setq index (1+ index))
        (setq button (next-button (button-end button))))
      (while button
        (setq buttons-after
              (cons (some-button--completion-candidate index button)
                    buttons-after))
        (setq index (1+ index))
        (setq button (next-button (button-end button))))
      (append (reverse buttons-after) (reverse buttons-before)))))

(defun to-line (str)
  "Inline STR."
  (when str
    (string-replace "\n" " " str)))

(defun some-button--completing-read (prompt collection window buffer)
  "Prompt for an button among COLLECTION with preview.
PROMPT passed on to `completing-read'.  WINDOW is the window in
which to show preview for locations in BUFFER."
  (define-advice next-completion (:after (&rest _) after-advice)
    (let* ((completion (with-minibuffer-completions-window
                         (substring-no-properties
                          (get-text-property (point)
                                             'completion--string))))
           (pos (cdr (assoc completion collection))))
      (with-selected-window window
        (unless (= (goto-char pos) (point))
          (widen)
          (goto-char pos))
        (recenter)
        (pulse-momentary-highlight-one-line))))
  (unwind-protect
      (let ((completions-sort nil)
            (completion-extra-properties
             (list :annotation-function (some-button--annotate-function collection buffer))))
        (completing-read prompt collection nil t nil nil (caar collection)))
    (advice-remove #'next-completion
                   'next-completion@after-advice)))

(defun some-button--annotate-function (collection buffer)
  "Annotate candidate amond COLLECTION in BUFFER."
  (lambda (key)
    (with-current-buffer buffer
      (let* ((button (button-at
                      (cdr
                       (assoc key collection))))
             (type (or (button-type button)
                       (button-get button 'action)))
             (url (button-get button 'shr-url))
             (turl (and url
                        (truncate-string-to-width
                         (to-line url) 80 nil ?\s t))))
        (to-line
         (if type
             (if turl
                 (format "\t%S %64s" type turl)
               (format "\t%S" type))
           (when turl
             (format "\t%64s" turl))))))))

;;;###autoload
(defun some-button (&optional no-push)
  "Jump to a button in the current buffer and push it.
If NO-PUSH is non-nil (interactively, the prefix argument), only
jump to the selected button but don't push it."
  (interactive "P")
  (if-let ((buf (current-buffer))
           (win (selected-window))
           (table (some-button--buttons)))
      (let* ((choice (save-excursion
                       (some-button--completing-read
                        "Button: " table win buf)))
             (pos (cdr (assoc choice table))))
        (goto-char pos)
        (unless no-push
          (or (ignore-errors (push-button))
              (shr-browse-url))))
    (user-error "No buttons in current buffer")))

(provide 'some-button)
;;; some-button.el ends here

Automatically update the completions buffer

;;; completions-auto-update.el --- Auto-update minibuffer completions  -*- lexical-binding: t; -*-

;; Copyright (C) 2023  Eshel Yaron

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

;; 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:

;; This file defines a minor mode `completions-auto-update-mode' that
;; updates the *Completions* buffer as you type in the minibuffer.

;;; Code:

(defcustom completions-auto-update-idle-time 0.2
  "Number of seconds of idle to wait for before updating *Completions*."
  :group 'minibuffer
  :type 'number)

(defvar-local completions-auto-update-timer nil)

(defun completions-auto-update ()
  "Update the *Completions* buffer, if it is visible."
  (when (get-buffer-window "*Completions*")
    (if completion-in-region-mode
        (completion-help-at-point)
      (minibuffer-completion-help)))
  (setq completions-auto-update-timer nil))

(defun completions-auto-update-start-timer ()
  "Start an idle timer for updating *Completions*."
  (unless (or completions-auto-update-timer
              (not (get-buffer-window "*Completions*")))
    (setq completions-auto-update-timer
          (run-with-idle-timer completions-auto-update-idle-time
                               nil #'completions-auto-update))))

(defun completions-auto-update-setup ()
  (add-hook 'post-self-insert-hook #'completions-auto-update-start-timer nil t))

(defun completions-auto-update-exit ()
  (remove-hook 'post-self-insert-hook #'completions-auto-update-start-timer t))

(define-minor-mode completions-auto-update-mode
  "Update the *Completions* buffer as you type in the minibuffer."
  :global t :group 'minibuffer
  (if completions-auto-update-mode
      (progn
        (add-hook 'minibuffer-setup-hook #'completions-auto-update-setup)
        (add-hook 'minibuffer-setup-exit #'completions-auto-update-exit))
    (remove-hook 'minibuffer-setup-hook #'completions-auto-update-setup)
    (remove-hook 'minibuffer-setup-exit #'completions-auto-update-exit)))

(provide 'completions-auto-update)
;;; completions-auto-update.el ends here

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")))
 '(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)))
 '(elisp-shorthand-font-lock-face ((t :inherit font-lock-keyword-face :slant italic)))
 '(elfeed-search-title-face ((t :inherit shadow))))

(custom-theme-set-variables
 'esy
 '(mode-line-format
   '(" %+ "
     (:eval (when-let (project (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"))
     (eglot--managed-mode (" " (:eval (when  (eglot--mode-line-format)))))
     "%n%])"
     (vc-mode vc-mode)
     mode-line-format-right-align
     (:eval (when (mode-line-window-selected-p)
              (list "" 'display-time-string 'battery-mode-line-string " ")))
     "%5l"
     (4 "|%c")
     " "
     (-3 "%p")
     "/%I "))
 '(elfeed-search-face-alist '((unread default)))
 '(mode-face-faces '(default fringe mode-line-inactive)))

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