;;; init.el --- bandali's emacs configuration -*- lexical-binding: t -*-

;; Copyright (C) 2018-2021  Amin Bandali <bandali@gnu.org>

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

;; GNU Emacs configuration of bandali, free software activist,
;; computing scientist, and GNU maintainer and volunteer.

;; Over the years, I've taken inspiration from configurations of many
;; great people.  Some that I can remember off the top of my head are:
;;
;; - https://github.com/dieggsy/dotfiles
;; - https://github.com/dakra/dmacs
;; - http://pages.sachachua.com/.emacs.d/Sacha.html
;; - https://github.com/dakrone/eos
;; - http://doc.rix.si/cce/cce.html
;; - https://github.com/jwiegley/dot-emacs
;; - https://github.com/wasamasa/dotemacs
;; - https://github.com/hlissner/doom-emacs

;;; Code:

;;; Emacs initialization

(defvar b/before-user-init-time (current-time)
  "Value of `current-time' when Emacs begins loading `user-init-file'.")
(defvar b/emacs-initialized nil
  "Whether Emacs has been initialized.")

(when (not (bound-and-true-p b/emacs-initialized))
  (message "Loading Emacs...done (%.3fs)"
           (float-time (time-subtract b/before-user-init-time
                                      before-init-time))))

;; temporarily increase `gc-cons-threshhold' and `gc-cons-percentage'
;; during startup to reduce garbage collection frequency.  clearing
;; `file-name-handler-alist' seems to help reduce startup time too.
(defvar b/gc-cons-threshold gc-cons-threshold)
(defvar b/gc-cons-percentage gc-cons-percentage)
(defvar b/file-name-handler-alist file-name-handler-alist)
(setq gc-cons-threshold (* 30 1024 1024)  ; 30 MiB
      gc-cons-percentage 0.6
      file-name-handler-alist nil
      ;; sidesteps a bug when profiling with esup
      esup-child-profile-require-level 0)

;; set them back to their defaults once we're done initializing
(defun b/post-init ()
  "My post-initialize function, run after loading `user-init-file'."
  (setq b/emacs-initialized     t
        gc-cons-threshold       b/gc-cons-threshold
        gc-cons-percentage      b/gc-cons-percentage
        file-name-handler-alist b/file-name-handler-alist)
  (when (featurep 'exwm-workspace)
    (with-eval-after-load 'exwm-workspace
      (setq-default
       mode-line-format
       (append
        mode-line-format
        '((:eval
           (format
            "[%s]" (number-to-string
                    exwm-workspace-current-index))))))))
  (when (version< emacs-version "28")
    ;; manually make some mode-line spaces smaller
    ;; (version<= "28" emacs-version) can do an awesome job at this
    ;; out of the box if `mode-line-compact' is set to t (see below)
    (setq-default
     mode-line-format
     (mapcar
      (lambda (x)
        (if (and (stringp x)
                 (or (string= x "   ")
                     (string= x "  ")))
            " "
          x))
      mode-line-format)
     mode-line-buffer-identification
     (propertized-buffer-identification "%10b"))))
(add-hook 'after-init-hook #'b/post-init)

;; increase number of lines kept in *Messages* log
(setq message-log-max 20000)


;;; whoami

(setq ;; user-full-name "bandali"
      user-mail-address "bandali@gnu.org")


;;; csetq (`custom' setq)

(require 'cl-lib)

(defmacro csetq (&rest args)
  "Set the value of user option VAR to VALUE.

More generally, you can use multiple variables and values, as in
  (csetq VAR VALUE VAR VALUE...)
This sets each user option VAR's value to the corresponding VALUE.

\(fn [VAR VALUE]...)"
  (declare (debug setq))
  `(progn
     ,@(cl-loop for (var value) on args by 'cddr
                collect
                `(funcall (or (get ',var 'custom-set) #'set-default)
                          ',var ,value))))


;;; Package management

;; variables of interest:
;;   package-archive-priorities
;;   package-load-list
;;   package-pinned-packages

;; (let* ((b (find-file-noselect "refinery-theme.el"))
;;        (d (with-current-buffer b (package-buffer-info))))
;;   (package-generate-description-file d "refinery-theme-pkg.el"))
(run-with-idle-timer 0.01 nil #'require 'package)
(with-eval-after-load 'package
  (csetq
   ;; package-archives
   ;; `(,@package-archives
   ;;   ("bndl" . "https://p.bndl.org/elpa/"))
   package-load-list
   '(;; GNU ELPA
     (debbugs "0.29")
     (delight "1.7")
     (emms "7.7")
     (expand-region "0.11.0")
     (rt-liberation "2.4")
     (yasnippet "0.14.0")))
(package-initialize))

(csetq package-archive-upload-base "/ssh:caffeine:~/www/p/elpa")


;;; Initial setup

;; keep ~/.emacs.d clean
(defvar b/etc-dir
  (expand-file-name
   (convert-standard-filename "etc/") user-emacs-directory)
  "The directory where packages place their configuration files.")
(defvar b/var-dir
  (expand-file-name
   (convert-standard-filename "var/") user-emacs-directory)
  "The directory where packages place their persistent data files.")
(defvar b/lisp-dir
  (expand-file-name
   (convert-standard-filename "lisp/") user-emacs-directory)
  "The directory where packages place their persistent data files.")
(defun b/etc (file)
  "Expand filename FILE relative to `b/etc-dir'."
  (expand-file-name (convert-standard-filename file) b/etc-dir))
(defun b/var (file)
  "Expand filename FILE relative to `b/var-dir'."
  (expand-file-name (convert-standard-filename file) b/var-dir))
(defun b/lisp (file)
  "Expand filename FILE relative to `b/lisp-dir'."
  (expand-file-name (convert-standard-filename file) b/lisp-dir))

(csetq
 auto-save-list-file-prefix (b/var "auto-save/sessions/")
 nsm-settings-file (b/var "nsm-settings.el"))

;; separate custom file (don't want it mixing with init.el)
(with-eval-after-load 'custom
  (setq custom-file (b/etc "custom.el"))
  (when (file-exists-p custom-file)
    (load custom-file))
  ;; while at it, treat themes as safe
  ;; (setf custom-safe-themes t)
  ;; only one custom theme at a time
  ;; (defadvice load-theme (before clear-previous-themes activate)
  ;;   "Clear existing theme settings instead of layering them"
  ;;   (mapc #'disable-theme custom-enabled-themes))
  )

;; load the secrets file if it exists, otherwise show a warning
;; (with-demoted-errors
;;     (load (b/etc "secrets")))

;; start up emacs server.  see
;; https://www.gnu.org/software/emacs/manual/html_node/emacs/Emacs-Server.html#Emacs-Server
(run-with-idle-timer 0.5 nil #'require 'server)
(with-eval-after-load 'server
  (declare-function server-edit "server")
  (global-set-key (kbd "C-c F D") #'server-edit)
  (declare-function server-running-p "server")
  (or (server-running-p) (server-mode)))


;;; Defaults

;;;; C-level customizations

(csetq
 ;; line-spacing 3
 ;; completion case sensitivity
 completion-ignore-case t
 read-buffer-completion-ignore-case t
 ;; minibuffer
 enable-recursive-minibuffers t
 resize-mini-windows t
 ;; mode-line
 mode-line-compact t
 ;; i don't feel like jumping out of my chair every now and again;
 ;; so...don't *BEEP* at me, emacs =)
 ring-bell-function 'ignore
 ;; better scrolling
 ;; scroll-conservatively 101
 scroll-conservatively 15
 ;; scroll-preserve-screen-position 1
 ;; focus follows mouse
 ;; mouse-autoselect-window t
 )

(setq-default
 ;; case-sensitive search (and `dabbrev-expand')
 ;; case-fold-search nil
 ;; always use space for indentation
 indent-tabs-mode nil
 tab-width 4)

(when (display-graphic-p)
  (set-fontset-font t 'arabic "Vazir"))
;; ;; (set-frame-font "Drafting Mono-14:weight=light" nil t)
;; (set-frame-font "Drafting Mono:pixelsize=16" nil t)
;; (set-face-attribute 'bold nil :weight 'semi-bold)

;;;; Elisp-level customizations

;; (define-key minibuffer-local-completion-map
;;   "\t" #'minibuffer-force-complete)

;; (with-eval-after-load 'icomplete

;; (setq icomplete-on-del-error-function #'abort-recursive-edit)

;; (defun b/icomplete-fido-backward-updir ()
;;   "Delete char before or go up directory, like `ido-mode'."
;;   (interactive)
;;   (if (and (eq (char-before) ?/)
;;            (eq (icomplete--category) 'file))
;;       (save-excursion
;;         (goto-char (1- (point)))
;;         (when (search-backward "/" (point-min) t)
;;           (delete-region (1+ (point)) (point-max))))
;;     (condition-case nil
;;         (call-interactively #'delete-backward-char)
;;       (error
;;        (when icomplete-on-del-error-function
;;          (funcall icomplete-on-del-error-function))))))

;; (define-key icomplete-fido-mode-map
;;   (kbd "DEL") #'b/icomplete-fido-backward-updir))

;; (with-eval-after-load 'subr
;;   (keyboard-translate ?\( ?\[)
;;   (keyboard-translate ?\) ?\])
;;   (keyboard-translate ?\[ ?\()
;;   (keyboard-translate ?\] ?\))

;;   ;; (keyboard-translate ?\( ?\()
;;   ;; (keyboard-translate ?\) ?\))
;;   ;; (keyboard-translate ?\[ ?\[)
;;   ;; (keyboard-translate ?\] ?\])
;; )

;; minibuffer
(csetq read-file-name-completion-ignore-case t)

;; startup
;; don't need to see the startup echo area message
(advice-add #'display-startup-echo-area-message :override #'ignore)
(csetq
 ;; i want *scratch* as my startup buffer
 initial-buffer-choice t
 ;; i don't need the default hint
 initial-scratch-message nil
 ;; use customizable text-mode as major mode for *scratch*
 ;; (initial-major-mode 'text-mode)
 ;; inhibit buffer list when more than 2 files are loaded
 inhibit-startup-buffer-menu t
 ;; don't need to see the startup screen or echo area message
 inhibit-startup-screen t
 inhibit-startup-echo-area-message user-login-name)

;; files
(csetq
 ;; backups (C-h v make-backup-files RET)
 backup-by-copying t
 backup-directory-alist (list (cons "." (b/var "backup/")))
 version-control t
 delete-old-versions t
 ;; auto-save
 auto-save-file-name-transforms `((".*" ,(b/var "auto-save/") t))
 ;; insert newline at the end of files
 ;; require-final-newline t
 ;; open read-only file buffers in view-mode
 ;; (enables niceties like `q' for quit)
 view-read-only t)

;; novice
;; disable disabled commands
(csetq disabled-command-function nil)

;; lazy-person-friendly yes/no prompts
(defalias 'yes-or-no-p #'y-or-n-p)

;; autorevert: enable automatic reloading of changed buffers and files
(csetq auto-revert-verbose nil
       global-auto-revert-non-file-buffers nil)
(require 'autorevert)
(global-auto-revert-mode 1)

;; time and battery in mode-line
(run-with-idle-timer 0.1 nil #'require 'time)
(with-eval-after-load 'time
  (csetq
   display-time-default-load-average nil
   display-time-format " %a %b %-e %-l:%M%P"
   display-time-mail-icon '(image :type xpm
                                  :file "gnus/gnus-pointer.xpm"
                                  :ascent center)
   display-time-use-mail-icon t)
  (display-time-mode))

(run-with-idle-timer 0.1 nil #'require 'battery)
(with-eval-after-load 'battery
  (csetq battery-mode-line-format " %p%% %t")
  (display-battery-mode))

;; (with-eval-after-load 'fringe
;;   ;; smaller fringe
;;   (fringe-mode '(3 . 1)))

;; enable winner-mode (C-h f winner-mode RET)
(require 'winner)
(winner-mode 1)

(with-eval-after-load 'compile
  ;; don't display *compilation* buffer on success.  based on
  ;; https://stackoverflow.com/a/17788551, with changes to use `cl-letf'
  ;; instead of the now obsolete `flet'.
  (defun b/compilation-finish-function (buffer outstr)
    (unless (string-match "finished" outstr)
      (switch-to-buffer-other-window buffer))
    t)

  (setq compilation-finish-functions #'b/compilation-finish-function)

  (require 'cl-macs)

  (defadvice compilation-start
      (around inhibit-display
              (command &optional mode name-function highlight-regexp))
    (if (not (string-match "^\\(find\\|grep\\)" command))
        (cl-letf (((symbol-function 'display-buffer) #'ignore))
          (save-window-excursion ad-do-it))
      ad-do-it))
  (ad-activate 'compilation-start))

;; isearch
(csetq
 ;; allow scrolling in Isearch
 isearch-allow-scroll t
 isearch-lazy-count t
 ;; search for non-ASCII characters: i’d like non-ASCII characters such
 ;; as ‘’“”«»‹›áⓐ𝒶 to be selected when i search for their ASCII
 ;; counterpart.  shoutout to
 ;; http://endlessparentheses.com/new-in-emacs-25-1-easily-search-non-ascii-characters.html
 search-default-mode #'char-fold-to-regexp)

;; replace
;; uncomment to extend the above behaviour to query-replace
;; (csetq replace-char-fold t)

;; vc
(global-set-key (kbd "C-x v C-=") #'vc-ediff)

(with-eval-after-load 'vc-git
  (csetq vc-git-print-log-follow t
         vc-git-show-stash 0))

(csetq ediff-window-setup-function 'ediff-setup-windows-plain
       ediff-split-window-function 'split-window-horizontally)
(with-eval-after-load 'ediff
  (add-hook 'ediff-after-quit-hook-internal #'winner-undo))

;; face-remap
(csetq
 ;; gentler font resizing
 text-scale-mode-step 1.05)

(run-with-idle-timer 0.4 nil #'require 'mwheel)
(csetq mouse-wheel-scroll-amount '(1 ((shift) . 1)) ; one line at a time
       mouse-wheel-progressive-speed nil ; don't accelerate scrolling
       mouse-wheel-follow-mouse t)       ; scroll window under mouse

(run-with-idle-timer 0.4 nil #'require 'pixel-scroll)
(with-eval-after-load 'pixel-scroll
  (pixel-scroll-mode 1))

;; epg-config
(csetq
 epg-gpg-program (executable-find "gpg")
 ;; ask for GPG passphrase in minibuffer
 ;; this will fail if gpg>=2.1 is not available
 epg-pinentry-mode 'loopback)

;; auth-source
(csetq
 auth-sources '("~/.authinfo.gpg")
 authinfo-hidden (regexp-opt '("password" "client-secret" "token")))

;; info
(with-eval-after-load 'info
  (add-to-list
   'Info-directory-list
   (expand-file-name
    (convert-standard-filename "info/") source-directory)))

;; faces
(when (display-graphic-p)
  (with-eval-after-load 'faces
    (let* ((grey "#e7e7e7")
           ;; (darker-grey "#d9d9d9")
           ;; (box ;; 'unspecified
           ;;      `(;; :line-width -1
           ;;        :style released-button))
           )
      (set-face-attribute 'mode-line nil
                          :background grey ;; :box box
                          )
      ;; (set-face-attribute 'mode-line-inactive nil
      ;;                     :background darker-grey :box box)
      )))


;;; Useful utilities

(defun b/add-elisp-section ()
  (interactive)
  (insert "\n")
  (forward-line -1)
  (insert "\n\n;;; "))

(defun b/insert-asterism ()
  "Insert a centred asterism."
  (interactive)
  (let ((asterism "* * *"))
    (insert
     (concat
      "\n"
      (make-string
       (floor (/ (- fill-column (length asterism)) 2))
       ?\s)
      asterism
      "\n"))))

(defun b/start-process (program &rest args)
  "Same as `start-process', but doesn't bother about name and buffer."
  (let ((process-name (concat program "_process"))
        (buffer-name  (generate-new-buffer-name
                       (concat program "_output"))))
    (apply #'start-process
           process-name buffer-name program args)))

(defun b/no-mouse-autoselect-window ()
  "Conveniently disable `focus-follows-mouse'.
For disabling the behaviour for certain buffers and/or modes."
  (make-local-variable 'mouse-autoselect-window)
  (setq mouse-autoselect-window nil))

(defun b/kill-current-buffer ()
  "Kill the current buffer."
  ;; also see https://redd.it/64xb3q
  (interactive)
  (kill-buffer (current-buffer)))

(defun b/move-indentation-or-beginning-of-line (arg)
  "Move to the indentation or to the beginning of line."
  (interactive "^p")
  ;; (if (bolp)
  ;;     (back-to-indentation)
  ;;   (move-beginning-of-line arg))
  (if (= (point)
         (progn (back-to-indentation)
                (point)))
      (move-beginning-of-line arg)))

(defun b/join-line-top ()
  "Like `join-line', but join next line to the current line."
  (interactive)
  (join-line 1))

(defun b/duplicate-line-or-region (&optional n)
  "Duplicate the current line, or region (if active).
Make N (default: 1) copies of the current line or region."
  (interactive "*p")
  (let ((u-r-p (use-region-p))          ; if region is active
        (n1 (or n 1)))
    (save-excursion
      (let ((text
             (if u-r-p
                 (buffer-substring (region-beginning) (region-end))
               (prog1 (thing-at-point 'line)
                 (end-of-line)
                 (if (eobp)
                     (newline)
                   (forward-line 1))))))
        (dotimes (_ (abs n1))
          (insert text))))))


;;; General key bindings

(global-set-key (kbd "C-a") #'b/move-indentation-or-beginning-of-line)
(global-set-key (kbd "C-c a i") #'ielm)
(global-set-key (kbd "C-c d") #'b/duplicate-line-or-region)
(global-set-key (kbd "C-c j") #'b/join-line-top)
(global-set-key (kbd "C-S-j") #'b/join-line-top)
(global-set-key (kbd "C-c x") #'execute-extended-command)

;; evaling and macro-expanding
(global-set-key (kbd "C-c e b") #'eval-buffer)
(global-set-key (kbd "C-c e e") #'eval-last-sexp)
(global-set-key (kbd "C-c e m") #'pp-macroexpand-last-sexp)
(global-set-key (kbd "C-c e r") #'eval-region)

;; emacs things
(global-set-key (kbd "C-c e i") #'emacs-init-time)
(global-set-key (kbd "C-c e u") #'emacs-uptime)
(global-set-key (kbd "C-c e v") #'emacs-version)

;; finding
(global-set-key (kbd "C-c f .") #'find-file)
(global-set-key (kbd "C-c f d") #'find-name-dired)
(global-set-key (kbd "C-c f l") #'find-library)
(global-set-key (kbd "C-c f p") #'find-file-at-point)

;; frames
(global-set-key (kbd "C-c F m") #'make-frame-command)
(global-set-key (kbd "C-c F d") #'delete-frame)

;; help/describe
(global-set-key (kbd "C-S-h F") #'describe-face)

;; (global-set-key (kbd "C-x k") #'b/kill-current-buffer)
;; (global-set-key (kbd "C-x K") #'kill-buffer)

(define-key emacs-lisp-mode-map (kbd "C-<return>") #'b/add-elisp-section)

(when (display-graphic-p)
  (global-unset-key (kbd "C-z")))


;;; Essential packages

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

;; (require 'bandali-exwm)

(require 'bandali-org)

;; (require 'bandali-theme)

;; recently opened files
(csetq recentf-max-saved-items 2000
       recentf-save-file (b/var "recentf-save.el"))
(run-with-idle-timer 0.2 nil #'require 'recentf)
(with-eval-after-load 'recentf
  ;; (add-to-list 'recentf-keep #'file-remote-p)
  (recentf-mode)

  (defun b/recentf-open ()
  "Use `completing-read' to \\[find-file] a recent file."
  (interactive)
  (find-file
   (completing-read "Find recent file: " recentf-list)))
  (global-set-key (kbd "C-c f r") #'b/recentf-open))

;; (fido-mode 1)
;; (defun b/icomplete--fido-mode-setup ()
;;   "Customizations to `fido-mode''s minibuffer."
;;   (when (and icomplete-mode (icomplete-simple-completing-p))
;;     (setq-local
;;      ;; icomplete-compute-delay 0.1
;;      ;; icomplete-hide-common-prefix t
;;      icomplete-separator " · "
;;      completion-styles '(basic substring partial-completion flex))))
;; (add-hook 'minibuffer-setup-hook #'b/icomplete--fido-mode-setup 1)

(require 'bandali-eshell)

(require 'bandali-ibuffer)

(require 'bandali-dired)

(with-eval-after-load 'help
  (temp-buffer-resize-mode)
  (csetq help-window-select t))

(with-eval-after-load 'help-mode
  ;; local key bindings
  (define-key help-mode-map (kbd "p") #'backward-button)
  (define-key help-mode-map (kbd "n") #'forward-button))

(with-eval-after-load 'tramp
  (csetq tramp-auto-save-directory (b/var "tramp/auto-save/")
         tramp-persistency-file-name (b/var "tramp/persistency.el"))
  (add-to-list 'tramp-default-proxies-alist '(nil "\\`root\\'" "/ssh:%h:"))
  (add-to-list 'tramp-default-proxies-alist '("localhost" nil nil))
  (add-to-list 'tramp-default-proxies-alist
               (list (regexp-quote (system-name)) nil nil)))

(with-eval-after-load 'doc-view
  (define-key doc-view-mode-map (kbd "M-RET") #'image-previous-line))

(csetq shr-max-width 80)

;; Email (with Gnus, message, and smtpmail)
(require 'bandali-gnus)
(require 'bandali-message)
;; (with-eval-after-load 'smtpmail
;;   (csetq smtpmail-queue-mail t
;;          smtpmail-queue-dir (concat b/maildir "queue/")))

;; IRC (with ERC)
(require 'bandali-erc)

;; 'paste' service (aka scp + web server)
(add-to-list 'load-path (b/lisp "scpaste"))
(with-eval-after-load 'scpaste
  (csetq scpaste-http-destination "https://p.bndl.org"
         scpaste-scp-destination "p:~"))
(autoload 'scpaste "scpaste" nil t)
(autoload 'scpaste-region "scpaste" nil t)
(global-set-key (kbd "C-c a p p") #'scpaste)
(global-set-key (kbd "C-c a p r") #'scpaste-region)


;;; Editing

;; display Lisp objects at point in the echo area
(when (version< "25" emacs-version)
  (with-eval-after-load 'eldoc
    (csetq eldoc-minor-mode-string " eldoc")
    (global-eldoc-mode)))

;; highlight matching parens
(require 'paren)
(show-paren-mode)

;; (require 'elec-pair)
;; (electric-pair-mode)

(csetq
 ;; Save what I copy into clipboard from other applications into Emacs'
 ;; kill-ring, which would allow me to still be able to easily access
 ;; it in case I kill (cut or copy) something else inside Emacs before
 ;; yanking (pasting) what I'd originally intended to.
 save-interprogram-paste-before-kill t)
(with-eval-after-load 'simple
  (column-number-mode 1)
  (line-number-mode 1))

;; save minibuffer history
(require 'savehist)
(csetq savehist-file (b/var "savehist.el"))
(savehist-mode)
(add-to-list 'savehist-additional-variables 'kill-ring)

;; automatically save place in files
(when (version< "25" emacs-version)
  (csetq save-place-file (b/var "save-place.el"))
  (save-place-mode))

(defun indicate-buffer-boundaries-left ()
    (csetq indicate-buffer-boundaries 'left))
(with-eval-after-load 'prog-mode
  (global-prettify-symbols-mode))
(add-hook 'prog-mode-hook #'indicate-buffer-boundaries-left)

(define-key text-mode-map (kbd "C-<return>") #'b/insert-asterism)
(add-hook 'text-mode-hook #'indicate-buffer-boundaries-left)
(add-hook 'text-mode-hook #'flyspell-mode)

(add-to-list 'auto-mode-alist '("\\.*rc$" . conf-mode))

(add-to-list 'auto-mode-alist '("\\.bashrc$" . sh-mode))

(with-eval-after-load 'flyspell
  (csetq flyspell-mode-line-string " fly"))

;; ispell
;; http://endlessparentheses.com/ispell-and-apostrophes.html
;; (run-with-idle-timer 0.6 nil #'require 'ispell)
;; (with-eval-after-load 'ispell
;;   ;; ’ can be part of a word
;;   (csetq ispell-local-dictionary-alist
;;          `((nil "[[:alpha:]]" "[^[:alpha:]]"
;;                 "['\x2019]" nil ("-B") nil utf-8))
;;          ispell-program-name (executable-find "hunspell"))
;;   ;; don't send ’ to the subprocess
;;   (defun endless/replace-apostrophe (args)
;;     (cons (replace-regexp-in-string
;;            "’" "'" (car args))
;;           (cdr args)))
;;   (advice-add #'ispell-send-string :filter-args
;;               #'endless/replace-apostrophe)
;;   ;; convert ' back to ’ from the subprocess
;;   (defun endless/replace-quote (args)
;;     (if (not (derived-mode-p 'org-mode))
;;         args
;;       (cons (replace-regexp-in-string
;;              "'" "’" (car args))
;;             (cdr args))))
;;   (advice-add #'ispell-parse-output :filter-args
;;               #'endless/replace-quote))

;; abbrev
(csetq abbrev-file-name (b/etc "abbrev.el"))
(add-hook 'text-mode-hook #'abbrev-mode)


;;; Programming modes

(with-eval-after-load 'lisp-mode
  (defun indent-spaces-mode ()
    (setq indent-tabs-mode nil))
  (add-hook 'lisp-interaction-mode-hook #'indent-spaces-mode))

;; alloy
(add-to-list 'load-path (b/lisp "alloy-mode"))
(autoload 'alloy-mode "alloy-mode" nil t)
(with-eval-after-load 'alloy-mode
  (csetq alloy-basic-offset 2)
  ;; (defun b/alloy-simple-indent (start end)
  ;;   (interactive "r")
  ;;   ;; (if (region-active-p)
  ;;   ;;     (indent-rigidly start end alloy-basic-offset)
  ;;   ;;   (if (bolp)
  ;;   ;;       (indent-rigidly (line-beginning-position)
  ;;   ;;                       (line-end-position)
  ;;   ;;                       alloy-basic-offset)))
  ;;   (indent-to (+ (current-column) alloy-basic-offset)))
  ;; local key bindings
  (define-key alloy-mode-map (kbd "RET") #'electric-newline-and-maybe-indent)
  ;; (define-key alloy-mode-map (kbd "TAB") #'b/alloy-simple-indent)
  (define-key alloy-mode-map (kbd "TAB") #'indent-for-tab-command))
(add-to-list 'auto-mode-alist '("\\.\\(als\\|dsh\\)\\'" . alloy-mode))
(add-hook 'alloy-mode-hook (lambda nil (setq-local indent-tabs-mode nil)))

;; lean
;; (eval-when-compile (defvar lean-mode-map))
;; (run-with-idle-timer 0.4 nil #'require 'lean-mode)
;; (with-eval-after-load 'lean-mode
;;   (require 'lean-input)
;;   (csetq default-input-method "Lean"
;;          lean-input-tweak-all '(lean-input-compose
;;                                 (lean-input-prepend "/")
;;                                 (lean-input-nonempty))
;;          lean-input-user-translations '(("/" "/")))
;;   (lean-input-setup)
;;   ;; local key bindings
;;   (define-key lean-mode-map (kbd "S-SPC") #'company-complete))

(with-eval-after-load 'sgml-mode
  (csetq sgml-basic-offset 0))

(with-eval-after-load 'css-mode
  (csetq css-indent-offset 2))

;; auctex
;; (csetq font-latex-fontify-sectioning 'color)

(with-eval-after-load 'tex-mode
  (cl-delete-if
   (lambda (p) (string-match "^---?" (car p)))
   tex--prettify-symbols-alist))
(add-hook 'tex-mode-hook #'auto-fill-mode)
(add-hook 'tex-mode-hook #'flyspell-mode)


;;; Emacs enhancements & auxiliary packages

(with-eval-after-load 'man
  (csetq Man-width 80))

(defun b/*scratch* ()
  "Switch to `*scratch*' buffer, creating it if it does not exist."
  (interactive)
  (switch-to-buffer
   (or (get-buffer "*scratch*")
       (with-current-buffer (get-buffer-create "*scratch*")
         (set-buffer-major-mode (current-buffer))
         (current-buffer)))))
(global-set-key (kbd "C-c s") #'b/*scratch*)

;; ,----
;; | make pretty boxed quotes like this
;; `----
(add-to-list 'load-path (b/lisp "boxquote"))
(run-with-idle-timer 0.6 nil #'require 'boxquote)
(with-eval-after-load 'boxquote
  (defvar b/boxquote-prefix-map)
  (define-prefix-command 'b/boxquote-prefix-map)
  (global-set-key (kbd "C-c q") 'b/boxquote-prefix-map)
  (define-key b/boxquote-prefix-map (kbd "b")   #'boxquote-buffer)
  (define-key b/boxquote-prefix-map (kbd "B")   #'boxquote-insert-buffer)
  (define-key b/boxquote-prefix-map (kbd "d")   #'boxquote-defun)
  (define-key b/boxquote-prefix-map (kbd "F")   #'boxquote-insert-file)
  (define-key b/boxquote-prefix-map (kbd "hf")  #'boxquote-describe-function)
  (define-key b/boxquote-prefix-map (kbd "hk")  #'boxquote-describe-key)
  (define-key b/boxquote-prefix-map (kbd "hv")  #'boxquote-describe-variable)
  (define-key b/boxquote-prefix-map (kbd "hw")  #'boxquote-where-is)
  (define-key b/boxquote-prefix-map (kbd "k")   #'boxquote-kill)
  (define-key b/boxquote-prefix-map (kbd "p")   #'boxquote-paragraph)
  (define-key b/boxquote-prefix-map (kbd "q")   #'boxquote-boxquote)
  (define-key b/boxquote-prefix-map (kbd "r")   #'boxquote-region)
  (define-key b/boxquote-prefix-map (kbd "s")   #'boxquote-shell-command)
  (define-key b/boxquote-prefix-map (kbd "t")   #'boxquote-text)
  (define-key b/boxquote-prefix-map (kbd "T")   #'boxquote-title)
  (define-key b/boxquote-prefix-map (kbd "u")   #'boxquote-unbox)
  (define-key b/boxquote-prefix-map (kbd "U")   #'boxquote-unbox-region)
  (define-key b/boxquote-prefix-map (kbd "y")   #'boxquote-yank)
  (define-key b/boxquote-prefix-map (kbd "M-q") #'boxquote-fill-paragraph)
  (define-key b/boxquote-prefix-map (kbd "M-w") #'boxquote-kill-ring-save))

(add-to-list 'load-path (b/lisp "hl-todo"))
(run-with-idle-timer 0.5 nil #'require 'hl-todo)
(with-eval-after-load 'hl-todo
  ;; highlight TODOs in buffers
  (global-hl-todo-mode))

;; expand-region
(global-set-key (kbd "C-=") #'er/expand-region)

(run-with-idle-timer 0.6 nil #'require 'yasnippet)
(with-eval-after-load 'yasnippet
  (declare-function yas-reload-all
                    "yasnippet" (&optional no-jit interactive))
  (declare-function yas-maybe-expand-abbrev-key-filter
                    "yasnippet" (cmd))

  (defconst yas-verbosity-cur yas-verbosity)
  (setq yas-verbosity 2)
  (csetq yas-snippet-dirs `(,(b/etc "yasnippet/snippets")))
  ;; (add-to-list 'yas-snippet-dirs "~/src/git/guix/etc/snippets" t)
  (yas-reload-all)
  (setq yas-verbosity yas-verbosity-cur)

  (defun b/yas-maybe-expand-abbrev-key-filter (cmd)
    (when (and (yas-maybe-expand-abbrev-key-filter cmd)
               (not (bound-and-true-p git-commit-mode)))
      cmd))
  (defconst b/yas-maybe-expand
    '(menu-item "" yas-expand
                :filter b/yas-maybe-expand-abbrev-key-filter))
  (define-key yas-minor-mode-map (kbd "SPC") b/yas-maybe-expand)

  (yas-global-mode))

;; debbugs
(global-set-key (kbd "C-c D d") #'debbugs-gnu)
(global-set-key (kbd "C-c D b") #'debbugs-gnu-bugs)
(global-set-key (kbd "C-c D e")         ; bug-gnu-emacs
                (lambda ()
                  (interactive)         
                  (setq debbugs-gnu-current-suppress t)
                  (debbugs-gnu debbugs-gnu-default-severities
                               '("emacs"))))
(global-set-key (kbd "C-c D g")         ; bug-gnuzilla
                (lambda ()
                  (interactive)
                  (setq debbugs-gnu-current-suppress t)
                  (debbugs-gnu debbugs-gnu-default-severities
                               '("gnuzilla"))))

;; url and url-cache
(csetq
 url-configuration-directory (b/var "url/configuration/")
 url-cache-directory (b/var "url/cache/"))

;; eww
(csetq eww-download-directory (file-name-as-directory
                               (getenv "XDG_DOWNLOAD_DIR")))
(global-set-key (kbd "C-c a e w") #'eww)

;; ;; org-ref
;; (csetq
;;  reftex-default-bibliography '("~/usr/org/references.bib")
;;  org-ref-default-bibliography '("~/usr/org/references.bib")
;;  org-ref-bibliography-notes "~/usr/org/notes.org"
;;  org-ref-pdf-directory "~/usr/org/bibtex-pdfs/")

;; fill-column-indicator ?

;; window
(csetq split-width-threshold 150)
(global-set-key (kbd "C-c w s l")
                (lambda ()
                  (interactive)
                  (split-window-right)
                  (other-window 1)))
(global-set-key (kbd "C-c w s j")
                (lambda ()
                  (interactive)
                  (split-window-below)
                  (other-window 1)))
(global-set-key (kbd "C-c w q") #'quit-window)

;; pass
;; (global-set-key (kbd "C-c a p") #'pass)
;; (add-hook 'pass-mode-hook #'View-exit)

;; reftex
;; uncomment to disable reftex-cite's default choice of previous word
;; (with-eval-after-load 'reftex
;;   (require 'reftex-cite)
;;   (defun reftex-get-bibkey-default ()
;;     "If the cursor is in a citation macro, return the word before the macro."
;;     (let* ((macro (reftex-what-macro 1)))
;;       (save-excursion
;;         (when (and macro (string-match "cite" (car macro)))
;;           (goto-char (cdr macro)))
;;         (reftex-this-word)))))
(add-hook 'latex-mode-hook #'reftex-mode)

;; dmenu
(add-to-list 'load-path (b/lisp "dmenu"))
(with-eval-after-load 'dmenu
  (csetq dmenu-prompt-string "run: "
         dmenu-save-file (b/var "dmenu-items")))
(autoload 'dmenu "dmenu" nil t)

;; eosd ?

;; delight
(run-with-idle-timer 0.5 nil #'require 'delight)
(with-eval-after-load 'delight
  (delight 'auto-fill-function " f" "simple")
  (delight 'abbrev-mode "" "abbrev")
  (delight 'mml-mode " mml" "mml")
  (delight 'yas-minor-mode "" "yasnippet"))

;; po-mode
(require 'bandali-po)

(with-eval-after-load 'emms
  (csetq emms-directory (b/var "emms")))


;;; Post initialization

(message "Loading %s...done (%.3fs)" user-init-file
         (float-time (time-subtract (current-time)
                                    b/before-user-init-time)))

;;; init.el ends here