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

;; Copyright (c) 2018-2026 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/>.


;;;; Initialization

(setq  ; invoking emacs with --debug-init sets `init-file-debug' to t
 debug-on-error init-file-debug
 debug-on-quit init-file-debug)

(setq  ; prefer newest version of a file
 load-prefer-newer t)

(setq  ; whoami
 user-full-name "Amin Bandali"
 user-mail-address "amin@shemshak.org")

(setq  ; make custom-file disposable
 custom-file (make-temp-file "emacs-custom-"))

(setq  ; don't resize the frame when font size is adjusted
 frame-resize-pixelwise t
 frame-inhibit-implied-resize 'force)

(setq  ; i don't like getting jump-scared out of my chair
 ring-bell-function #'ignore)

(progn ; disable some distractions (*)
  ;; (*) if you're new to emacs, i suggest keeping the menu bar and
  ;;     tool bar enabled for a while - they can be very helpful
  ;;     in helping you (re)discover context-specific or general
  ;;     functionality as you get more acquainted with emacs :)
  (menu-bar-mode -1)
  (when (fboundp #'tool-bar-mode)
    (tool-bar-mode -1))
  (when (fboundp #'scroll-bar-mode)
    (scroll-bar-mode -1))
  (blink-cursor-mode -1))

(progn ; startup and gc tweaks for faster initialization
  (defconst bandali--gc-cons-threshold gc-cons-threshold)
  (defconst bandali--gc-cons-percentage gc-cons-percentage)
  (defvar bandali--file-name-handler-alist file-name-handler-alist)
  (defvar bandali--vc-handled-backends vc-handled-backends)
  (setq
   gc-cons-threshold (* 30 1024 1024) ; 30 MiB
   gc-cons-percentage 0.6
   file-name-handler-alist nil
   vc-handled-backends nil)
  (defun bandali--post-init ()
    "My post-initialization function."
    (setq  ; restore the defaults
     gc-cons-threshold bandali--gc-cons-threshold
     gc-cons-percentage bandali--gc-cons-percentage
     file-name-handler-alist bandali--file-name-handler-alist
     vc-handled-backends bandali--vc-handled-backends))
  (add-hook 'after-init-hook #'bandali--post-init))

;; (when (boundp 'pgtk-wait-for-event-timeout)
;;   (setq pgtk-wait-for-event-timeout 0.001))


;;;; Core

(defvar bandali--configure-times nil
  "Stores execution times of `bandali-configure' configuration blocks.")

(defmacro bandali-configure (name &rest body)
  "Evaluate BODY and catch any errors.
NAME will be included in the error report to help the user more easily
find the configuration block.

The execution time of BODY will be added to `bandali--configure-times'
for later inspection and/or reporting.

With inspiration from Protesilaos's `prot-emacs-configure' and Eshel
Yaron's `esy/init-step'."
  (declare (indent 1)
           (doc-string 1))
  (let ((start-time-symbol (gensym)))
    `(let ((,start-time-symbol (current-time)))
       (prog1
           (condition-case err
               (progn ,@body)
             ((error user-error quit)
              (message
               "bandali-configure: error in block `%S' due to `%S'"
               ',name (cdr err))))
         (push
          (cons ',name
                (time-subtract (current-time) ,start-time-symbol))
          bandali--configure-times)))))

;; Usage examples:

;; (bandali-configure package
;;   (with-eval-after-load 'package
;;     (setopt package-review-policy t)))

;; (bandali-configure "tabs and spaces"
;;   (setopt tab-always-indent 'complete)
;;   (setq-default
;;    indent-tabs-mode nil
;;    tab-width 4))

(defun bandali-configure-report-times (&optional sort)
  "Report execution times of `bandali-configure' blocks.
With optional SORT as a prefix argument, if greater than or equal to
zero, or one or more \\[universal-argument], the execusion times are
reported in order of increasing time.  If SORT is a negative number or
just `-', then the execution times are reported in order of decreasing
time.  Otherwise, if SORT is nil or not provided, the execution times
are reported as-is without any sorting, in order of occurrence."
  (interactive "P")
  (let* ((times-list (if (null sort)
                         (reverse bandali--configure-times)
                       (sort
                        bandali--configure-times
                        :key #'cdr
                        :lessp #'time-less-p
                        :reverse
                        (or (and (numberp sort) (< sort 0))
                            (and (symbolp sort) (eq sort '-))))))
         (times-str (mapconcat
                     (lambda (name-time)
                       (format "%f %s"
                               (float-time (cdr name-time))
                               (car name-time)))
                     times-list
                     "\n")))
    (if (called-interactively-p)
        (message "(bandali-configure) execution times:\n%s" times-str)
      times-str)))

;; The `bandali-define-key' convenience macro is a wrapper around
;; Emacs's `define-key', taking a sequence of keys and definitions,
;; allowing binding a key sequence to a command, nil to unset it,
;; a keymap, or anything else allowed by `define-key' itself.

(defmacro bandali-define-key (keymap &rest definitions)
  "Expand key binding DEFINITIONS for the given KEYMAP.
DEFINITIONS is a sequence of string and command pairs.
With inspiration from Protesilaos's `prot-emacs-keybind'."
  (declare (indent 1))
  (unless (zerop (% (length definitions) 2))
    (error "Uneven number of key+command pairs"))
  `(when-let* (((keymapp ,keymap))
               (map ,keymap))
     ,@(mapcar
        (lambda (pair)
          (pcase-let ((`(,key ,def) pair))
            (unless (and (null key) (null def))
              `(define-key map ,(if (stringp key) `(kbd ,key) key) ,def))))
        (seq-partition definitions 2))))

;; Usage example:

;; (bandali-define-key global-map
;;   "C-z" nil
;;   "C-x b" #'switch-to-buffer
;;   "C-x k" #'kill-buffer
;;   "C-c p" project-prefix-map)


;;;; Simple commands and utilities

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

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

(defun bandali-*scratch* ()
  "Switch to `*scratch*' buffer, creating it if it does not exist."
  (interactive)
    (switch-to-buffer (get-scratch-buffer-create)))

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

(defun bandali-invert-default-face (arg)
  "Invert the `default' and `mode-line' faces for the current frame.
Swap the background and foreground for the two `default' and
`mode-line' faces, effectively acting like a simple light/dark
theme toggle.  If prefix argument ARG is given, invert the faces
for all frames."
  (interactive "P")
  (let ((frame (unless arg
                 (selected-frame))))
    (invert-face 'default frame)
    (invert-face 'mode-line frame)
    (when (fboundp 'exwm-systemtray--refresh-background-color)
      (exwm-systemtray--refresh-background-color 'remap))))

(defun bandali-unfill-paragraph-or-region (&optional beg end)
  "Unfill paragraph, or region (if active)."
  (interactive "r")
  (let ((fill-column most-positive-fixnum))
    (if (use-region-p)
        (fill-region beg end)
      (fill-paragraph))))

(defun bandali-pactl-get-volume (&optional type name)
  "Returns current Pulse volume from `pactl' if possible, else `nil'.
By default, it will return the volume of the default sink.

Optional TYPE must be either \"sink\" or \"source\".

Optional NAME should be the name of the sink/source whose volume should
be retrieved.  If not provided, the default sink/source will be used."
  (interactive
   (list
    (completing-read "Type (default sink): "
                     '("sink" "source")
                     nil t nil nil "sink")
    (let ((n (read-string "Sink/source name: ")))
      (unless (string-empty-p n) n))))
  (let* ((s_t (or type "sink"))
         (s_n (or name (format "@DEFAULT_%s@" (upcase s_t))))
         (out (condition-case err
                  (process-lines
                   "pactl" (format "get-%s-volume" s_t) s_n)
                (error
                 (message "Error invoking pactl: %s"
                          (error-message-string err))
                 nil))))
    (catch 'vol_str
      (dolist (line out)
        (when (string-match "\\([[:digit:]]+\\)%" line)
          (throw 'vol_str (match-string 1 line)))))))

(defun bandali-pactl-set-default-sink-volume (volume)
  "Try to set VOLUME of the default sink using `pactl'."
  (interactive
   ;; read `volume' as string so we can differentiate 5 from +5
   (list
    (read-string
     (format-prompt "sink volume" (bandali-pactl-get-volume "sink")))))
  (start-process
   "" nil
   "pactl" "set-sink-volume" "@DEFAULT_SINK@" (format "%s%%" volume)))

(defun bandali-pactl-set-default-source-volume (volume)
  "Try to set VOLUME of the default source using `pactl'."
  (interactive
   ;; read `volume' as string so we can differentiate 5 from +5
   (list
    (read-string
     (format-prompt "source volume" (bandali-pactl-get-volume "source")))))
  (start-process
   "" nil
   "pactl" "set-sink-volume" "@DEFAULT_SOURCE@" (format "%s%%" volume)))

(defun bandali-brightnessctl-get ()
  "Returns current brightness using `brightnessctl'."
  (interactive)
  (let ((out (condition-case err
                 (process-lines "brightnessctl" "-m")
               (error
                (message "Error invoking brightnessctl: %s"
                         (error-message-string err))
                nil))))
    (catch 'brightness_str
      (dolist (line out)
        (when (string-match "\\([[:digit:]]+\\)%" line)
          (throw 'brightness_str (match-string 1 line)))))))

(defun bandali-brightnessctl-set (level)
  "Try to set brightness to LEVEL percent using `brightnessctl'."
  (interactive
   (list
    (read-string
     (format-prompt "brightness" (bandali-brightnessctl-get)))))
  (start-process "" nil "brightnessctl" "s" (format "%s%%" level)))

(bandali-define-key global-map
  "C-c s c" #'bandali-*scratch*
  "C-c d" #'bandali-duplicate-line-or-region
  "C-c j" #'bandali-join-line-top
  "C-c v" #'bandali-invert-default-face
  "C-c q" #'bandali-unfill-paragraph-or-region)


;;;; Theme & appearance

(when (or (display-graphic-p)
          (string= (getenv "COLORTERM") "truecolor"))
  (require 'doric-themes)
  (setopt doric-themes-to-toggle '(doric-oak doric-pine))
  (doric-themes-select 'doric-oak)
  (with-eval-after-load 'calendar
    (doric-themes-with-colors
      (set-face-attribute
       'diary
       nil
       :background bg-shadow-subtle
       :foreground fg-shadow-subtle))))

(when (display-graphic-p)
  (set-fontset-font t 'arabic "Sahel WOL")

  (let ((emoji-font "Apple Color Emoji"))
    (when (member emoji-font (font-family-list))
      (set-fontset-font
       t 'emoji `(,emoji-font . "iso10646-1") nil 'prepend)))

  (with-eval-after-load 'faces
    (let ((font "Source Code Pro Medium"))
      (set-face-attribute 'default nil :font font :height 115)
      ;; (set-face-attribute 'fixed-pitch nil :inherit 'default)
      )))

(setq-default
 bidi-paragraph-start-re "^"
 bidi-paragraph-separate-re "^")

(with-eval-after-load 'face-remap
  (setopt
   text-scale-mode-step 1.05
   text-scale-remap-header-line t))

;; Emacs 29 introduced global-text-scale-adjust
(bandali-define-key global-map
  "C-x C-+" #'global-text-scale-adjust
  "C-x C-=" #'global-text-scale-adjust
  "C-x C--" #'global-text-scale-adjust
  "C-x C-0" #'global-text-scale-adjust
  "C-x C-M-+" #'text-scale-adjust
  "C-x C-M-=" #'text-scale-adjust
  "C-x C-M--" #'text-scale-adjust
  "C-x C-M-0" #'text-scale-adjust)

(run-with-idle-timer
 0.2 nil #'require 'display-fill-column-indicator nil 'noerror)
(with-eval-after-load 'display-fill-column-indicator
  ;; (add-hook 'prog-mode-hook #'display-fill-column-indicator-mode)
  ;; (add-hook 'message-mode-hook #'display-fill-column-indicator-mode)
  (global-display-fill-column-indicator-mode 1))


;;;; Essentials

(setopt
 initial-buffer-choice t
 initial-major-mode #'lisp-interaction-mode
 initial-scratch-message
 (format
  ";; this is `%s' - eval & print results with `%s'\n\n"
  #'lisp-interaction-mode
  (substitute-command-keys
   "\\<lisp-interaction-mode-map>\\[eval-print-last-sexp]")))

(run-with-idle-timer 0.5 nil #'require 'server)
(with-eval-after-load 'server
  (eval-when-compile
    (declare-function server-edit "server")
    (declare-function server-running-p "server")
    (declare-function server-start "server"))
  (bandali-define-key global-map "C-c s e r" #'server-edit)
  (unless (server-running-p)
    (server-start)))

(defvar bandali-fundamental-mode-hook nil
  "A hook for `fundamental-mode'.")

(advice-add
 #'fundamental-mode
 :around
 (lambda (&rest args)
   (apply args)
   (run-hooks 'bandali-fundamental-mode-hook)))

(setopt tab-always-indent 'complete)
(setq-default
 indent-tabs-mode nil
 tab-width 4)

(setq-default indicate-buffer-boundaries 'left)

(mapc
 (lambda (command)
   (put command 'disabled nil))
 '( narrow-to-page narrow-to-region
    upcase-region downcase-region
    diff-restrict-view list-timers))

(mapc
 (lambda (command)
   (put command 'disabled t))
 '( overwrite-mode iconify-frame))

(with-eval-after-load 'package
  (setopt
   ;; Explicitly set `package-archives', in part to ensure https ones
   ;; are used, and also to have NonGNU ELPA on older Emacsen as well.
   package-archives
   '(("gnu" . "https://elpa.gnu.org/packages/")
     ("gnu-devel" . "https://elpa.gnu.org/devel/")
     ("nongnu" . "https://elpa.nongnu.org/nongnu/"))
   package-archive-priorities
   '(("gnu" . 3)
     ("gnu-devel" . 2)
     ("nongnu" . 1))
   package-pinned-packages
   '((doric-themes . "gnu-devel"))
   package-review-policy t))

(defvar Info-directory-list)
(with-eval-after-load 'info
  (setq
   Info-directory-list
   `(,@Info-directory-list
     ,(expand-file-name
       (convert-standard-filename "info/") source-directory)
     "/usr/share/info/")))

(run-with-idle-timer 0.2 nil #'require 'recentf)
(with-eval-after-load 'recentf
  (setopt recentf-max-saved-items 100)
  (recentf-mode 1)
  (bandali-define-key global-map
    "C-c f r e" #'recentf-open)

  (declare-function recentf-add-file "recentf")
  (defun bandali-recentf-add-dir-if-not-home ()
    "Add `default-directory' to `recentf-list' if we're not at $HOME."
    (unless
        (string=
         (file-name-as-directory (expand-file-name default-directory))
         (file-name-as-directory (expand-file-name (getenv "HOME"))))
      (recentf-add-file default-directory)))

  (declare-function
   bandali-recentf-add-dir-if-not-home "bandali-essentials")
  (with-eval-after-load 'vc-dir
    (add-hook 'vc-dir-mode-hook #'bandali-recentf-add-dir-if-not-home))
  (with-eval-after-load 'dired
    (add-hook 'dired-mode-hook #'bandali-recentf-add-dir-if-not-home)))

(run-with-idle-timer 0.4 nil #'require 'mwheel)
(with-eval-after-load 'mwheel
  (setopt
   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

(setq mouse-yank-at-point t)

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

(setopt
 ;; mouse-autoselect-window t
 scroll-conservatively 15
 scroll-preserve-screen-position 1)

(run-with-idle-timer 0.1 nil #'require 'autorevert)
(with-eval-after-load 'autorevert
  (setopt
   global-auto-revert-non-file-buffers nil)
  (global-auto-revert-mode 1))

(run-with-idle-timer 0.5 nil #'require 'repeat)
(with-eval-after-load 'repeat
  (setopt
   repeat-exit-key "RET"
   repeat-exit-timeout nil)
  (repeat-mode 1))

(defvar zoneinfo-style-world-list)
(run-with-idle-timer 0.1 nil #'require 'time)
(with-eval-after-load 'time
  (setopt
   ;; display-time-default-load-average nil
   display-time-format " %a %-d %b %-l:%M%P"
   display-time-mail-icon
   '(image :type xpm :file "gnus/gnus-pointer.xpm" :ascent center)
   display-time-use-mail-icon t
   zoneinfo-style-world-list
   '(("America/Los_Angeles" "San Francisco")
     ("America/Toronto" "Toronto")
     ("Etc/UTC" "UTC")
     ("Europe/Athens" "Cyprus")
     ("Asia/Tehran" "Tehran")))
  (unless (display-graphic-p)
    (display-time-mode 1)))

(defvar bandali-battery-format "%p%b %t")
(run-with-idle-timer 0.1 nil #'require 'battery)
(with-eval-after-load 'battery
  (setopt
   battery-mode-line-format (format " [%s]" bandali-battery-format))
  (unless (display-graphic-p)
    (display-battery-mode 1)))

(setopt
 trusted-content
 `(,(file-name-as-directory (locate-user-emacs-file "user-lisp/ffs"))))

(with-eval-after-load 'eat
  ;; (setq process-adaptive-read-buffering t)
  (setopt
   ;; eat-enable-shell-prompt-annotation nil
   eat-enable-shell-command-history nil)
  ;; (bandali-define-key eat-char-mode-map
  ;;   "<remap> <mouse-yank-primary>" #'eat-mouse-yank-primary
  ;;   "<remap> <mouse-yank-secondary>" #'eat-mouse-yank-secondary
  ;;   "M-RET" #'eat-line-mode)
  ;; (bandali-define-key eat-line-mode-map "M-RET" #'eat-char-mode)
  ;; (add-hook 'eat-exec-hook (lambda (_) (eat-char-mode)))
  (add-hook
   'eat-mode-hook
   (lambda ()
     (with-eval-after-load 'display-fill-column-indicator
       (display-fill-column-indicator-mode -1))))
  (with-eval-after-load 'info
    (add-to-list 'Info-directory-list
                 (locate-user-emacs-file "lisp/eat"))))
(defun bandali-eat ()
  (interactive)
  (let ((current-prefix-arg '(4))) ; C-u
    (call-interactively #'eat)))
(bandali-define-key global-map "C-c s e t" #'bandali-eat)

(with-eval-after-load 'select
  (setopt
   select-enable-clipboard t
   ;; select-enable-primary t
   ))

(setq
 ;; line-spacing 3
 delete-by-moving-to-trash t
 max-mini-window-height 0.20
 ;; resize-mini-windows t
 message-log-max 20000)

(with-eval-after-load 'files
  (setopt
   make-backup-files nil
   ;; Insert newline at the end of files.
   ;; require-final-newline t
   ;; Open read-only file buffers in view-mode, to get `q' for quit.
   view-read-only t))
(bandali-define-key global-map "C-c f ." #'find-file)

(with-eval-after-load 'epg-config
  (setopt
   epg-gpg-program (executable-find "gpg")
   ;; Ask for GPG passphrase in minibuffer.
   ;; Will fail if gpg >= 2.1 is not available.
   epg-pinentry-mode 'loopback))

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

(with-eval-after-load 'help-mode
  (bandali-define-key help-mode-map
    "P" #'backward-button
    "N" #'forward-button
    "b" #'help-go-back
    "f" #'help-go-forward))

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

(with-eval-after-load 'tramp
  (setopt
   remote-file-name-inhibit-locks t
   remote-file-name-inhibit-auto-save-visited t
   tramp-default-method "rsync"
   tramp-copy-size-limit (* 1024 1024) ;; 1MB
   tramp-verbose 2)
  (connection-local-set-profile-variables
   'remote-direct-async-process
   '((tramp-direct-async-process . t)))
  (connection-local-set-profiles
   '(:application tramp :protocol "scp")
   'remote-direct-async-process)
  (tramp-set-completion-function
   "ssh"
   (append (tramp-get-completion-function "ssh")
           (mapcar (lambda (file) `(tramp-parse-sconfig ,file))
                   (directory-files
                    "~/.ssh/config.d/"
                    'full directory-files-no-dot-files-regexp)))))

(with-eval-after-load 'simple
  (setopt
   ;; See `bandali-gnus' for my Gnus configuration.
   mail-user-agent 'gnus-user-agent
   read-mail-command #'gnus
   ;; 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)
  (column-number-mode 1)
  (line-number-mode 1))
(bandali-define-key global-map "C-x k" #'kill-current-buffer)
;; (add-hook 'text-mode-hook #'auto-fill-mode)
;; (add-hook 'tex-mode-hook #'auto-fill-mode)

;; Save minibuffer history.
(run-with-idle-timer 0.2 nil #'require 'savehist)
(with-eval-after-load 'savehist
  (savehist-mode 1)
  (add-to-list 'savehist-additional-variables 'kill-ring))

;; Automatically save place in files.
(run-with-idle-timer 0.2 nil #'require 'saveplace nil 'noerror)
(with-eval-after-load 'saveplace
  (save-place-mode 1))

;; `abbrev'
(add-hook 'text-mode-hook #'abbrev-mode)

(with-eval-after-load 'calendar
  (setopt
   calendar-date-style 'iso
   calendar-mark-diary-entries-flag t
   diary-file "~/usr/doc/diary"))
(bandali-define-key global-map "C-c c" #'calendar)
(with-eval-after-load 'diary-lib
  (setopt
   diary-comment-start ";")
  (add-hook 'diary-list-entries-hook 'diary-sort-entries t))
(if (version<= "31" emacs-version)
    (setopt
     holiday-other-holidays
     '((holiday-float 10 1 2 "Canadian Thanksgiving")))
  (setq ; must be `setq'd before `holidays' is loaded
   holiday-other-holidays
   '((holiday-float 10 1 2 "Canadian Thanksgiving"))))
(with-eval-after-load 'appt
  (setopt
   appt-display-diary nil
   appt-display-format nil
   appt-display-mode-line t
   appt-display-interval 5
   appt-message-warning-time 20))
(add-hook
 'after-init-hook
 (lambda ()
   (require 'appt)
   (appt-activate 1)))

;; `ffs'
(add-hook
 'ffs-present-mode-hook
 (lambda ()
   (let ((arg (if ffs-present-mode -1 1)))
     (mapc
      (lambda (mode) (funcall mode arg))
      '(show-paren-local-mode
        display-fill-column-indicator-mode
        flyspell-mode)))
   (if ffs-present-mode
       (fringe-mode 0)
     (fringe-mode nil))))

(defun bandali-call-interactively-insert (command string)
  "Execute interactive COMMAND with STRING prefilled in minibuffer.
Returns a lambda that when executed will execute COMMAND interactively,
with STRING inserted into the minibuffer.  Useful for binding to a key."
  (lambda ()
    (interactive)
    (let ((cmd command)
          (str string))
      (minibuffer-with-setup-hook
          (lambda () (insert str))
        (call-interactively cmd)))))
(declare-function
 bandali-call-interactively-insert "bandali-essentials")

(bandali-define-key global-map
  "C-z" nil ; `suspend-frame' is already bound to `C-x C-z'
  ;; `time'
  "C-c e i" #'emacs-init-time
  "C-c e u" #'emacs-uptime
  ;; `version'
  "C-c e v" #'emacs-version
  ;; `ffap'
  "C-c f p" #'find-file-at-point
  ;; `find-func'
  "C-c f l" #'find-library
  ;; `frame'
  "C-c f r m" #'make-frame-command
  "C-c f r d" #'delete-frame
  ;; `help'-related
  "C-c h a" (bandali-call-interactively-insert
             #'execute-extended-command "apropos-")
  "C-c h d" (bandali-call-interactively-insert
             #'execute-extended-command "describe-")
  "C-c h f" (bandali-call-interactively-insert
             #'execute-extended-command "find-")
  ;; `simple'
  "M-=" #'count-words
  "M-o" #'delete-blank-lines
  "M-c" #'capitalize-dwim
  "M-l" #'downcase-dwim
  "M-u" #'upcase-dwim
  ;; `misc'
  "M-z" #'zap-up-to-char)


;;;; Window

(with-eval-after-load 'frame
  (undelete-frame-mode 1))
(bandali-define-key global-map
  "C-c f r u" #'undelete-frame)

(defvar bandali-exwm-machines '("adelita" "marita")
  "Host name of machines where I use EXWM.")

(defvar bandali-hidpi-machines '(("marita" . 144))
  "Host name and DPI of of machines with HiDPI display.")

(when (and
       (display-graphic-p)
       ;; we're not running in another WM/DE
       (not (or
             (getenv "XDG_CURRENT_DESKTOP")
             (getenv "WAYLAND_DISPLAY")))
       (member (system-name) bandali-exwm-machines))
  (require 'exwm)

  (bandali-define-key global-map
    "C-x b" #'exwm-workspace-switch-to-buffer)

  (let ((e "emacsclient"))
    (setenv "EDITOR" e)
    (setenv "VISUAL" e))

  (menu-bar-mode -1)
  (tool-bar-mode -1)

  (defun bandali-exwm-rename-buffer ()
    "Make class name the buffer name, truncating beyond 25 characters."
    (interactive)
    (exwm-workspace-rename-buffer
     (concat exwm-class-name ":"
             (if (<= (length exwm-title) 25) exwm-title
               (concat (substring exwm-title 0 24) "...")))))

  ;; Initial number of workspaces
  (setopt exwm-workspace-number 5)

  (defvar bandali-shifted-ws-names
    '( 0 \) 1 \! 2 \@ 3 \# 4 \$
       5 \% 6 \^ 7 \& 8 \* 9 \()
    "Mapping of shifted numbers on my keyboard.")

  (defvar-keymap bandali-prefix-browser-launch-map
    :doc "Prefix keymap for launching browsers."
    :name "Browser launch"
    :prefix 'bandali-prefix-browser-launch
    "c" (lambda () (interactive)
          (start-process
           "" nil "chromium"))
    "C" (lambda () (interactive)
          (start-process
           "" nil "chromium" "--incognito"))
    "f" (lambda () (interactive)
          (start-process
           "" nil "b-browser" "-P" "farangis"))
    "F" (lambda () (interactive)
          (start-process
           "" nil "b-browser" "-P" "farangis" "-private-window"))
    "i" (lambda () (interactive)
          (start-process
           "" nil "b-browser" "-P" "ia"))
    "I" (lambda () (interactive)
          (start-process
           "" nil "b-browser" "-P" "ia" "-private-window"))
    "p" (lambda () (interactive)
          (start-process
           "" nil "b-browser" "-P" "personal"))
    "P" (lambda () (interactive)
          (start-process
           "" nil "b-browser" "-P" "personal" "-private-window")))

  (defvar-keymap bandali-prefix-exwm-map
    :doc "Prefix keymap for EXWM."
    :name "EXWM"
    :prefix 'bandali-prefix-exwm
    "r" #'exwm-reset ; to line mode
    "w" #'exwm-workspace-switch
    "SPC" #'async-shell-command
    "RET" #'bandali-eat
    "C-k" (lambda () (interactive) (exwm-manage--kill-client))
    "t" (lambda () (interactive) (start-process "" nil "xterm"))
    "T" (lambda () (interactive)
          (start-process "" nil "xterm" "-name" "floating"))
    "h" #'windmove-left
    "j" #'windmove-down
    "k" #'windmove-up
    "l" #'windmove-right
    "H" #'windmove-swap-states-left
    "J" #'windmove-swap-states-down
    "K" #'windmove-swap-states-up
    "L" #'windmove-swap-states-right
    "M-h" #'shrink-window-horizontally
    "M-j" #'enlarge-window
    "M-k" #'shrink-window
    "M-l" #'enlarge-window-horizontally
    "p" #'bandali-exwm-ws-prev
    "n" #'bandali-exwm-ws-next
    "P" #'bandali-exwm-move-ws-prev
    "N" #'bandali-exwm-move-ws-next
    "," (lambda () (interactive) (other-frame -1))
    ";" #'bandali-pactl-set-default-sink-volume
    ":" #'bandali-pactl-set-default-source-volume
    "'" #'bandali-brightnessctl-set
    "." #'exwm-floating-toggle-floating
    "f" #'exwm-layout-toggle-fullscreen
    "b" #'bandali-prefix-browser-launch)

  (defvar exwm-workspace--create-silently)
  (defun bandali-exwm-workspace-create (frame-or-index)
    "Create (up to) workspace FRAME-OR-INDEX without switching to it."
    (interactive
     (list
      (cond
       ((integerp current-prefix-arg)
        current-prefix-arg)
       (t 0))))
    (unless frame-or-index
      (setq frame-or-index 0))
    (unless (or (framep frame-or-index)
                (< frame-or-index (exwm-workspace--count)))
      (let ((count (1+ (- frame-or-index (exwm-workspace--count))))
            (exwm-workspace--create-silently t))
        (when (< count exwm-workspace-switch-create-limit)
          (dotimes (_ count) (make-frame))
          (run-hooks 'exwm-workspace-list-change-hook)))))

  (mapc
   (lambda (i)
     (bandali-define-key bandali-prefix-exwm-map
       (format "%d" i)
       (lambda () (interactive) (exwm-workspace-switch-create i))
       (format "%s" (plist-get bandali-shifted-ws-names i))
       (lambda ()
         (interactive)
         (bandali-exwm-workspace-create i)
         (exwm-workspace-move-window i))))
   (number-sequence 0 (1- exwm-workspace-switch-create-limit)))

  (defvar-keymap bandali-prefix-exwm-mvmt-repeat-map
    :doc "Keymap to repeat EXWM movement commands.  Used with `repeat-mode'."
    :repeat t
    "h" #'windmove-left
    "j" #'windmove-down
    "k" #'windmove-up
    "l" #'windmove-right
    "H" #'windmove-swap-states-left
    "J" #'windmove-swap-states-down
    "K" #'windmove-swap-states-up
    "L" #'windmove-swap-states-right
    "p" #'bandali-exwm-ws-prev
    "n" #'bandali-exwm-ws-next
    "P" #'bandali-exwm-move-ws-prev
    "N" #'bandali-exwm-move-ws-next)

  (with-eval-after-load 'exwm-input
    (push ?\s-, exwm-input-prefix-keys)
    (push ?\s-x exwm-input-prefix-keys))

  (setq
   ;; Global keybindings
   exwm-input-global-keys
   `(([?\C-c ?x] . bandali-prefix-exwm)
     ([?\s-x] . bandali-prefix-exwm)
     ([?\s-,] . bandali-prefix-exwm)
     ([XF86AudioMute]
      .
      (lambda ()
        (interactive)
        (start-process
         "" nil
         "pactl" "set-sink-mute" "@DEFAULT_SINK@" "toggle")))
     ([XF86AudioLowerVolume]
      .
      (lambda ()
        (interactive)
        (start-process
         "" nil
         "pactl" "set-sink-volume" "@DEFAULT_SINK@" "-5%")))
     ([XF86AudioRaiseVolume]
      .
      (lambda ()
        (interactive)
        (start-process
         "" nil
         "pactl" "set-sink-volume" "@DEFAULT_SINK@" "+5%"))))
   ;; Line-editing shortcuts
   exwm-input-simulation-keys
   '(;; movement
     ([?\C-b] . [left])
     ([?\M-b] . [C-left])
     ([?\C-f] . [right])
     ([?\M-f] . [C-right])
     ([?\C-p] . [up])
     ([?\C-n] . [down])
     ([?\C-a] . [home])
     ([?\C-e] . [end])
     ([?\M-v] . [prior])
     ([?\C-v] . [next])
     ([?\C-d] . [delete])
     ([?\C-k] . [S-end ?\C-x])
     ([?\M-<] . C-home)
     ([?\M->] . C-end)
     ;; selection/cut/copy/paste
     ([?\s-a] . [?\C-a])
     ([?\C-w] . [?\C-x])
     ([?\M-w] . [?\C-c])
     ([?\C-y] . [?\C-v])
     ([?\M-d] . [C-S-right ?\C-x])
     ([?\M-\d] . [C-S-left ?\C-x])
     ;; closing/quite
     ([?\s-w] . [?\C-w])
     ([?\s-q] . [?\C-q])
     ;; misc
     ([?\C-s] . [?\C-f])
     ([?\s-d] . [?\C-d])
     ([?\s-g] . [?\C-g])
     ([?\s-s] . [?\C-s])
     ([?\C-g] . [escape])
     ([?\C-/] . [?\C-z])))

  (with-eval-after-load 'exwm-layout
    (setopt exwm-layout-fullscreen-release-keyboard nil))

  (with-eval-after-load 'exwm-manage
    (setq
     exwm-manage-configurations
     '(((equal exwm-instance-name "floating")
        ;; char-mode t
        floating t
        ;; floating-mode-line nil
        )
       ((member exwm-class-name '("Mate-terminal"))
        char-mode t)))
    (add-hook
     'exwm-manage-finish-hook
     (lambda ()
       (when exwm-class-name
         (cond
          ((member exwm-class-name '("XTerm" "Mate-terminal"))
           (exwm-input-set-local-simulation-keys
            '(([?\C-c ?\C-c] . [?\C-c])
              ([?\C-c ?\C-u] . [?\C-u]))))
          ((string= exwm-class-name "Zathura")
           (exwm-input-set-local-simulation-keys
            '(([?\C-p] . [C-up])
              ([?\C-n] . [C-down])))))))))

  ;; Enable EXWM
  (exwm-wm-mode 1)
  (add-hook 'exwm-update-class-hook #'bandali-exwm-rename-buffer)
  (add-hook 'exwm-update-title-hook #'bandali-exwm-rename-buffer)

  (when (executable-find "dunst")
    (defvar bandali--dunst-process
      (start-process "dunst" "*dunst*" "dunst"))
    (set-process-query-on-exit-flag bandali--dunst-process nil)
    (when (executable-find "dunstctl")
      (bandali-define-key bandali-prefix-exwm-map
        "d RET" (lambda () (interactive)
                  (start-process "" nil "dunstctl" "context"))
        "d d" (lambda () (interactive)
                (start-process "" nil "dunstctl" "close"))
        "d D" (lambda () (interactive)
                (start-process "" nil "dunstctl" "close-all"))
        "d r" (lambda () (interactive)
                (start-process "" nil "dunstctl" "history-pop")))))

  (require 'exwm-input)
  (defun bandali-exwm-ws-prev-index (&optional arg)
    "Return the index for the previous EXWM workspace, wrapping around if needed."
    (let ((max (if arg
                   exwm-workspace-switch-create-limit
                 (exwm-workspace--count))))
      (if (<= exwm-workspace-current-index 0)
          (1- max)
        (1- exwm-workspace-current-index))))

  (defun bandali-exwm-ws-next-index (&optional arg)
    "Return the index for the next EXWM workspace, wrapping around if needed."
    (let ((max (if arg
                   exwm-workspace-switch-create-limit
                 (exwm-workspace--count))))
      (if (>= exwm-workspace-current-index (1- max))
          0
        (1+ exwm-workspace-current-index))))

  (defun bandali-exwm-ws-prev (&optional arg)
    "Switch to previous EXWM workspace, wrapping around if needed.
If prefix argument is set, allow going beyond currently existing
workspaces and create new ones, respecting
`exwm-workspace-switch-create-limit'."
    (interactive "P")
    (exwm-workspace-switch-create
     (bandali-exwm-ws-prev-index arg)))

  (defun bandali-exwm-ws-next (&optional arg)
    "Switch to next EXWM workspace, wrapping around if needed.
If prefix argument is set, allow going beyond currently existing
workspaces and create new ones, respecting
`exwm-workspace-switch-create-limit'."
    (interactive "P")
    (exwm-workspace-switch-create
     (bandali-exwm-ws-next-index arg)))

  (defun bandali-exwm-move-ws-prev ()
    "Move window to previous workspace."
    (interactive)
    (exwm-workspace-move-window (bandali-exwm-ws-prev-index)))

  (defun bandali-exwm-move-ws-next ()
    "Move window to next workspace."
    (interactive)
    (exwm-workspace-move-window (bandali-exwm-ws-next-index)))


  ;; Shorten 'C-c C-q' to 'C-q'
  (define-key exwm-mode-map [?\C-q] #'exwm-input-send-next-key)

  (add-hook
   'exwm-init-hook
   (lambda ()
     (setq my-last-frame (selected-frame))))

  (add-hook
   'exwm-init-hook
   (lambda ()
     ;; Scroll up/down/left/right on the mode line
     (bandali-define-key global-map
       "<mode-line> <wheel-up>" #'bandali-exwm-ws-prev
       "<mode-line> <wheel-down>" #'bandali-exwm-ws-next
       "<mode-line> <wheel-left>" #'bandali-exwm-ws-prev
       "<mode-line> <wheel-right>" #'bandali-exwm-ws-next)))

  ;; Scroll up/down/left/right on the echo area
  (bandali-define-key minibuffer-inactive-mode-map
    [wheel-up] #'bandali-exwm-ws-prev
    [wheel-down] #'bandali-exwm-ws-next
    [wheel-left] #'bandali-exwm-ws-prev
    [wheel-right] #'bandali-exwm-ws-next)

  (require 'exwm-systemtray)
  (exwm-systemtray-mode 1)

  ;; (add-to-list 'load-path (locate-user-emacs-file "lisp/exwm-edit"))
  ;; (require 'exwm-edit)

  (defun bandali-exwm-xsettings ()
    (let* ((host-dpi (assoc (system-name) bandali-hidpi-machines))
           (dpi (if (and host-dpi
                         (= (length (display-monitor-attributes-list))
                            1))
                    (cdr host-dpi)
                  96)))
      `(("Xft/Hinting" . 1)
        ("Xft/AutoHint" . 0)
        ("Xft/HintStyle" . "hintslight")
        ("Xft/Antialias" . 1)
        ("Xft/RGBA" . "rgb")
        ("Xft/lcdfilter" . "lcddefault")
        ;; DPI is in 1024ths of an inch
        ("Xft/DPI" . ,(* dpi 1024)))))

  (with-eval-after-load 'exwm-xsettings
    (setopt exwm-xsettings (bandali-exwm-xsettings)))
  (require 'exwm-xsettings)
  (exwm-xsettings-mode 1)

  (require 'exwm-randr)
  (add-hook
   'exwm-randr-screen-change-hook
   (lambda ()
     (let ((xrandr-output-regexp "\n\\([^ ]+\\) connected ")
           default-output)
       (with-temp-buffer
         (call-process "xrandr" nil t nil)
         (goto-char (point-min))
         (re-search-forward xrandr-output-regexp nil 'noerror)
         (setq default-output (match-string 1))
         (forward-line)
         (if (not (re-search-forward xrandr-output-regexp nil 'noerror))
             (progn
               (call-process
                "xrandr" nil nil nil "--auto")
               (call-process
                "xrandr" nil nil nil "--output" default-output
                "--auto"))
           (call-process
            "xrandr" nil nil nil
            "--output" (match-string 1)
            "--right-of" default-output
            "--auto"
            "--output" default-output "--mode" "1920x1080")
           (setopt
            exwm-randr-workspace-monitor-plist
            (mapcan
             (lambda (i)
               (list i (if (< i 2) default-output (match-string 1))))
             (number-sequence
              0 (1- exwm-workspace-switch-create-limit)))))))))
  (add-hook
   'exwm-randr-refresh-hook
   (lambda () (setopt exwm-xsettings (bandali-exwm-xsettings))))
  (exwm-randr-mode 1)

  (with-eval-after-load 'exwm-workspace
    (bandali-define-key exwm-workspace-switch-map
      "p" #'next-history-element
      "n" #'previous-history-element)
    (setq exwm-workspace-show-all-buffers t)
    ;; Display current EXWM workspace in mode-line
    (setq-default
     mode-line-format
     (append
      mode-line-format
      '((:eval
         (format
          " [%s]" (number-to-string
                   exwm-workspace-current-index))))))))

(with-eval-after-load 'window
  (setopt
   display-buffer-alist
   '(("\\`\\*Group\\*\\'"
      (display-buffer-reuse-mode-window display-buffer-in-tab)
      (mode . gnus-group-mode)
      (tab-name . "Gnus")
      (inhibit-switch-frame . t))
     ("\\`\\*vc-dir\\*"
      (display-buffer-reuse-mode-window display-buffer-in-tab)
      (mode . vc-dir-mode)
      (tab-name . (lambda (buffer _alist) (buffer-name buffer))))
     ("\\`*vc-git"
      (display-buffer-reuse-window display-buffer-pop-up-window)
      (body-function . select-window))
     ((derived-mode . calendar-mode)
      (display-buffer-reuse-mode-window display-buffer-below-selected)
      (mode . calendar-mode)
      (inhibit-switch-frame . t)
      (dedicated . t)
      (window-height . fit-window-to-buffer))
     ((derived-mode . diary-mode)
      (display-buffer-in-side-window)
      (dedicated . t)
      (side . bottom)
      (slot . 0)
      (window-height . 0.3)
      ;; (window-parameters . ((mode-line-format . none)))
      ))
   split-width-threshold 140))

(defun bandali-delete-window-dwim ()
  "Do What I Mean to delete the current THING.

THING is determined in the following order:
When there is more than one window, THING is a window.
When there is more than one tab, THING is a tab.
When there is more than one frame, THING is a frame.

Based on Protesilaos's `prot-simple-delete-window-dwim'."
  (declare (interactive-only t))
  (interactive)
  (cond
   ((length> (window-list) 1)
    (delete-window))
   ((length> (tab-bar-tabs) 1)
    (tab-close))
   ((length> (frame-list) 1)
    (delete-frame))
   (t
    (user-error "Nothing to delete"))))
(bandali-define-key global-map "C-x 0" #'bandali-delete-window-dwim)

(run-with-idle-timer 0.5 nil #'require 'winner)
(with-eval-after-load 'winner
  (winner-mode 1)
  (when (featurep 'exwm)
    ;; prevent a bad interaction between EXWM and winner-mode, where
    ;; sometimes closing a window (like closing a terminal after
    ;; entering a GPG password via pinentry-gnome3's floating window)
    ;; results in a dead frame somewhere and effectively freezes EXWM.
    (advice-add
     'winner-insert-if-new
     :around
     (lambda (orig-fun &rest args)
       ;; only add the frame if it's live
       (when (frame-live-p (car args))
         (apply orig-fun args))))))

(run-with-idle-timer 0.5 nil #'require 'windmove)
(with-eval-after-load 'windmove
  (setopt windmove-wrap-around t))


;;;; Mode line and tab bar

(setopt
 mode-line-compact nil ; Emacs 28
 mode-line-right-align-edge 'right-margin) ; Emacs 30

(with-eval-after-load 'doric-themes
  (defun bandali-mode-line-set-faces ()
    (doric-themes-with-colors
      (custom-set-faces
       ;; "Padding" for mode lines
       `(mode-line
         ((t :box (:line-width 6 :color ,bg-shadow-intense))))
       `(mode-line-inactive
         ((t :box (:line-width 6 :color ,bg-shadow-subtle))))
       `(mode-line-highlight
         ((t :box (:color ,bg-shadow-intense)))))))
  (add-hook
   'doric-themes-after-load-theme-hook #'bandali-mode-line-set-faces)
  (bandali-mode-line-set-faces))

(with-eval-after-load 'keycast
  (setopt
   keycast-mode-line-format "%2s%k%c%R"
   keycast-mode-line-insert-after 'mode-line-misc-info
   keycast-mode-line-window-predicate 'mode-line-window-selected-p
   keycast-mode-line-remove-tail-elements nil)

  (dolist (input '(self-insert-command
                   org-self-insert-command
                   isearch-printing-char))
    (add-to-list 'keycast-substitute-alist `(,input "." "Typing…")))

  (dolist (event '("<mouse-event>" "<mouse-movement>" "<mouse-2>"
                   "<drag-mouse-1>" "<wheel-up>" "<wheel-down>"
                   "<double-wheel-up>" "<double-wheel-down>"
                   "<triple-wheel-up>" "<triple-wheel-down>"
                   "<wheel-left>" "<wheel-right>"
                   handle-select-window mouse-set-point
                   mouse-drag-region))
    (add-to-list 'keycast-substitute-alist `(,event nil nil))))

(with-eval-after-load 'tab-bar
  (setopt
   tab-bar-close-button-show nil
   tab-bar-new-button-show nil
   tab-bar-show 1))


;;;; Completion

(setopt
 completion-ignore-case t
 read-buffer-completion-ignore-case t)
(with-eval-after-load 'minibuffer
  (setopt
   completion-show-help nil
   completion-show-inline-help nil
   completion-auto-help t
   completion-eager-display 'auto
   completion-eager-update t
   completion-category-overrides
   '((file . ((eager-display . nil)
              (styles basic partial-completion substring emacs22 flex)))
     (buffer . ((styles basic partial-completion substring emacs22 flex))))
   completion-styles
   '(basic partial-completion substring emacs22)
   completions-detailed t
   completions-format 'one-column
   completions-max-height 11
   completions-sort 'historical
   minibuffer-visible-completions t
   read-file-name-completion-ignore-case t))
(setq-default case-fold-search t)

(setopt
 minibuffer-prompt-properties
 '(read-only t cursor-intangible t face minibuffer-prompt))
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)

(with-eval-after-load 'crm
  (setopt
   crm-prompt (format "%s %%p" (propertize "[%d]" 'face 'shadow)))

   ;; `completing-read-multiple' prompt indicator for older Emacsen
   ;; https://bugs.gnu.org/76028
   (when (< emacs-major-version 31)
     (advice-add
      #'completing-read-multiple :filter-args
      (lambda (args)
        (cons (format "[CRM%s] %s"
                      (string-replace "[ \t]*" "" crm-separator)
                      (car args))
              (cdr args))))))

(setopt enable-recursive-minibuffers t)
(run-with-idle-timer 0.5 nil #'require 'mb-depth)
(with-eval-after-load 'mb-depth
  (minibuffer-depth-indicate-mode 1))


;;;; Search

(with-eval-after-load 'isearch
  (setopt
   isearch-allow-scroll 'unlimited
   isearch-lazy-count t
   ;; Match non-ASCII variants during search
   search-default-mode #'char-fold-to-regexp))

(with-eval-after-load 'replace
  (setopt query-replace-show-preview #'replace-preview-both))


;;;; Languages

(with-eval-after-load 'mule-cmds
  (setopt default-input-method "farsi-isiri-9147"))

;; (with-eval-after-load 'flyspell
;;   (setopt flyspell-mode-line-string " fly"))
(add-hook 'text-mode-hook #'flyspell-mode)
(add-hook 'tex-mode-hook #'flyspell-mode)

(with-eval-after-load 'files
  (add-to-list
   'auto-mode-alist '("\\(README.*\\|\\(COMMIT\\|TAG\\)_EDITMSG$\\)" . text-mode))
  (add-to-list 'auto-mode-alist '("\\.*rc$" . conf-mode))
  (add-to-list 'auto-mode-alist '("\\.bashrc$" . sh-mode)))

;; `elisp-mode'
;; (with-eval-after-load 'elisp-mode
;;   (setopt elisp-fontify-semantically t))
(bandali-define-key global-map "C-c e e" #'eval-last-sexp)

;; Display Lisp objects at point in the echo area.
(with-eval-after-load 'eldoc
  (setopt eldoc-minor-mode-string " eldoc")
  (global-eldoc-mode 1))

;; Highlight matching parens.
(run-with-idle-timer 0.2 nil #'require 'paren)
(with-eval-after-load 'paren
  (setopt
   show-paren-context-when-offscreen 'overlay
   ;; show-paren-style 'expression
   show-paren-when-point-in-periphery t
   show-paren-when-point-inside-paren t)
  (show-paren-mode 1))

(with-eval-after-load 'text-mode
  ;; Treat single-quote as punctuation
  (modify-syntax-entry ?' ".   " text-mode-syntax-table))
(when (version<= "31" emacs-version)
  (bandali-define-key global-map "C-c C" #'center-line-mode))

;; `pp'
(bandali-define-key global-map "C-c e m" #'pp-macroexpand-last-sexp)

;; `lisp-mode'
(add-hook
 'lisp-interaction-mode-hook
 (lambda () (setq-local indent-tabs-mode nil)))

(with-eval-after-load 'sgml-mode
  (setopt sgml-basic-offset 0))
(add-hook 'sgml-mode-hook (lambda () (electric-indent-local-mode -1)))

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

;; `reftex'
(add-hook 'latex-mode-hook #'reftex-mode)

;; `po-mode'
(defvar po-mode-map)
(declare-function po-mode "po-mode")
(declare-function View-exit "view")

(with-eval-after-load 'po-mode
  ;; Based on the `po-wrap' function from the GNUN manual:
  ;; https://www.gnu.org/s/trans-coord/manual/gnun/html_node/Wrapping-Long-Lines.html
  (defun bandali-po-wrap ()
    "Run the current `po-mode' buffer through `msgcat' to wrap all
lines."
    (interactive)
    (when (eq major-mode 'po-mode)
      (let ((tmp-file (make-temp-file "po-wrap."))
            (tmp-buffer (generate-new-buffer "*temp*")))
        (unwind-protect
            (progn
              (write-region (point-min) (point-max) tmp-file nil 1)
              (if (zerop
                   (call-process "msgcat" nil tmp-buffer t
                                 (shell-quote-argument tmp-file)))
                  (let ((saved (point))
                        (inhibit-read-only t))
                    (delete-region (point-min) (point-max))
                    (insert-buffer-substring tmp-buffer)
                    (goto-char (min saved (point-max))))
                (with-current-buffer tmp-buffer
                  (error (buffer-string)))))
          (kill-buffer tmp-buffer)
          (delete-file tmp-file)))))
  (declare-function bandali-po-wrap "bandali-lang")

  (add-hook
   'po-mode-hook (lambda () (run-with-timer 0.1 nil #'View-exit)))
  (bandali-define-key po-mode-map "M-q" #'bandali-po-wrap))


;;;; Dired

(with-eval-after-load 'dired
  (setopt
   dired-dwim-target t
   ;; dired-listing-switches "-alh --group-directories-first"
   dired-listing-switches "-alh")

  (declare-function dired-dwim-target-directory "dired-aux")
  ;; easily diff 2 marked files
  ;; https://oremacs.com/2017/03/18/dired-ediff/
  (defun dired-ediff-files ()
    (interactive)
    (require 'dired-aux)
    (defvar ediff-after-quit-hook-internal)
    (let ((files (dired-get-marked-files))
          (wnd (current-window-configuration)))
      (if (<= (length files) 2)
          (let ((file1 (car files))
                (file2 (if (cdr files)
                           (cadr files)
                         (read-file-name
                          "file: "
                          (dired-dwim-target-directory)))))
            (if (file-newer-than-file-p file1 file2)
                (ediff-files file2 file1)
              (ediff-files file1 file2))
            (add-hook 'ediff-after-quit-hook-internal
                      (lambda ()
                        (setq ediff-after-quit-hook-internal nil)
                        (set-window-configuration wnd))))
        (error "no more than 2 files should be marked"))))

  ;; local key bindings
  (bandali-define-key dired-mode-map
    "b" #'dired-up-directory
    "E" #'dired-ediff-files
    "e" #'dired-toggle-read-only
    "\\" #'dired-hide-details-mode)

  (require 'dired-x)
  (setopt
   dired-guess-shell-alist-user
   '(("\\.pdf\\'"  "atril" "evince" "okular")
     ("\\.doc\\'"  "libreoffice")
     ("\\.docx\\'" "libreoffice")
     ("\\.ppt\\'"  "libreoffice")
     ("\\.pptx\\'" "libreoffice")
     ("\\.xls\\'"  "libreoffice")
     ("\\.xlsx\\'" "libreoffice")
     ("\\.flac\\'" "mpv"))))
(add-hook 'dired-mode-hook #'dired-hide-details-mode)
(add-hook 'dired-mode-hook #'hl-line-mode)


;;;; VC

(with-eval-after-load 'ediff
  (setopt
   ediff-window-setup-function #'ediff-setup-windows-plain
   ediff-split-window-function #'split-window-horizontally))

(with-eval-after-load 'project
  (setopt
   project-vc-extra-root-markers '("Makefile")
   project-vc-merge-submodules nil))

(with-eval-after-load 'vc
  (setopt vc-allow-rewriting-published-history 'ask))
(bandali-define-key global-map "C-x v C-=" #'vc-ediff)

(with-eval-after-load 'vc-dir
  (setopt vc-dir-show-key-binding-hints nil))

(with-eval-after-load 'vc-git
  (setopt vc-git-log-switches "--format=fuller")
  (when (version< emacs-version "30")
    (setopt vc-git-print-log-follow t)))

(with-eval-after-load 'vc-hooks
  (setopt vc-use-incoming-outgoing-prefixes t))

(when (and (boundp 'global-diff-hl-mode)
           (functionp #'global-diff-hl-mode))
  (global-diff-hl-mode 1))


;;;; Org

(with-eval-after-load 'org
  (setopt
   org-directory "~/usr/doc/org"
   org-agenda-files (list org-directory)
   org-src-content-indentation 0
   org-src-preserve-indentation t
   org-src-window-setup 'current-window))

(with-eval-after-load 'org-agenda
  (require 'appt)
  (appt-activate 1)
  ;; Create reminders for tasks with a due date when agenda is read.
  (org-agenda-to-appt))

(with-eval-after-load 'ox
  ;; (setopt org-export-timestamp-file nil)
  (require 'ox-texinfo))

(with-eval-after-load 'ox-ascii
  (setopt
   org-ascii-inner-margin 0
   org-ascii-text-width 70))

(with-eval-after-load 'ox-html
  (setopt
   org-html-doctype "xhtml5"
   org-html-html5-fancy t
   org-html-container-element "section"
   org-html-divs '((preamble  "header"  "preamble")
                   (content   "article" "content")
                   (postamble "footer"  "postamble"))))


;;;; Gnus

;; (defvar bandali-maildir
;;   (expand-file-name (convert-standard-filename "~/mail/")))

(eval-when-compile
  (progn
    (defvar nndraft-directory)
    (defvar gnus-read-newsrc-file)
    (defvar gnus-save-newsrc-file)
    (defvar gnus-gcc-mark-as-read)
    (defvar nnmail-split-abbrev-alist)))

(declare-function article-make-date-line "gnus-art" (date type))

(with-eval-after-load 'gnus
  (setopt
   gnus-select-method '(nnnil "")
   gnus-secondary-select-methods
   `((nnimap
      "shemshak"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "amin@shemshak.local")
      ;; (nnmail-expiry-wait immediate)
      (nnmail-expiry-target nnmail-fancy-expiry-target)
      (nnmail-fancy-expiry-targets
       ((to-from "bandali@debian\\.org" "nnimap+debian:Archive.%Y")
        ("envelope-to" "bandali\\+debian@kelar\\.org" "nnimap+debian:Archive.%Y")
        ("from" ".*" "nnimap+shemshak:Archive.%Y"))))
     (nnimap
      "ia"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "bandali@archive.local"))
     (nnimap
      "debian"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "bandali@debian.local")
      ;; (nnmail-expiry-wait immediate)
      (nnmail-expiry-target nnmail-fancy-expiry-target)
      (nnmail-fancy-expiry-targets
       (("from" ".*" "nnimap+debian:Archive.%Y"))))
     (nnimap
      "gnu"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "bandali@gnu.local")
      (nnimap-inbox "INBOX")
      (nnimap-split-methods 'nnimap-split-fancy)
      (nnimap-split-fancy
       (|
        ;; (: gnus-registry-split-fancy-with-parent)
        ;; (: gnus-group-split-fancy "INBOX" t "INBOX")
        ;; spam
        ("X-Spam_action" "reject" "Junk")
        ;; keep debbugs emails in INBOX
        (list ".*<\\(.*\\)\\.debbugs\\.gnu\\.org>.*" "INBOX")
        ;; list moderation emails
        (from ".+-\\(owner\\|bounces\\)@\\(non\\)?gnu\\.org" "listmod")
        ;; gnu
        (to "gnumaint-reply@gnu\\.org" "l.gnumaint-reply")
        (list ".*<\\(.*\\)\\.\\(non\\)?gnu\\.org>.*" "l.\\1")
        ("Envelope-To" "emacsconf-donations@gnu.org" "l.emacsconf-donations")
        ;; board-eval
        (|
         (list ".*<.*\\.board-eval\\.fsf\\.org>.*" "l.board-eval")
         (from ".*@board-eval\\.fsf\\.org" "l.board-eval"))
        ;; fsf
        (list ".*<\\(.*\\)\\.fsf\\.org>.*" "l.\\1")
        ;; cfarm
        (from "cfarm-.*@lists\\.tetaneutral\\.net" "l.cfarm")
        ;; debian
        (list ".*<\\(.*\\)\\.\\(lists\\|other\\)\\.debian\\.org>.*" "l.\\1")
        (list ".*<\\(.*\\)\\.alioth-lists\\.debian\\.net>.*" "l.\\1")
        ;; gnus
        (list ".*<\\(.*\\)\\.gnus\\.org>.*" "l.\\1")
        ;; libreplanet
        (list ".*<\\(.*\\)\\.libreplanet\\.org>.*" "l.\\1")
        ;; iana (e.g. tz-announce)
        (list ".*<\\(.*\\)\\.iana\\.org>.*" "l.\\1")
        ;; mailop
        (list ".*<\\(.*\\)\\.mailop\\.org>.*" "l.\\1")
        ;; sdlu
        (list ".*<\\(.*\\)\\.spammers\\.dontlike\\.us>.*" "l.sdlu")
        ;; bitfolk
        (from ".*@\\(.+\\)?bitfolk\\.com>.*" "bitfolk")
        ;; haskell
        (list ".*<\\(.*\\)\\.haskell\\.org>.*" "l.\\1")
        ;; webmasters
        (from "webmasters\\(-comment\\)?@gnu\\.org" "webmasters")
        ;; other
        (list ".*atreus.freelists.org" "l.atreus")
        (list ".*deepspec.lists.cs.princeton.edu" "l.deepspec")
        (list ".*haskell-art.we.lurk.org" "l.haskell-art")
        (list ".*dev.lists.parabola.nu" "l.parabola-dev")
        ;; otherwise, leave mail in INBOX
        "INBOX")))
     (nnimap
      "csc"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "abandali@csclub.uwaterloo.local")
      (nnimap-inbox "INBOX")
      (nnimap-split-methods 'nnimap-split-fancy)
      (nnimap-split-fancy
       (|
        ;; cron reports and other messages from root
        (from "root@\\(.*\\.\\)?csclub\\.uwaterloo\\.ca" "INBOX")
        ;; spam
        ("X-Spam-Flag" "YES" "Junk")
        ;; catch-all
        "INBOX"))))
   gnus-message-archive-group "nnimap+shemshak:INBOX"
   gnus-parameters
   '(("l\\.fencepost-users"
      (to-address . "fencepost-users@gnu.org")
      (to-list    . "fencepost-users@gnu.org")
      (list-identifier . "\\[Fencepost-users\\]"))
     ("l\\.haskell-cafe"
      (to-address . "haskell-cafe@haskell.org")
      (to-list    . "haskell-cafe@haskell.org")
      (list-identifier . "\\[Haskell-cafe\\]")))
   ;; (gnus-large-newsgroup  50)
   gnus-process-mark-toggle t
   gnus-home-directory (locate-user-emacs-file "gnus/")
   gnus-directory
   (expand-file-name
    (convert-standard-filename "news/") gnus-home-directory)
   gnus-interactive-exit nil)

  (with-eval-after-load 'message
    (setopt
     message-directory
     (expand-file-name
      (convert-standard-filename "mail/") gnus-home-directory)))

  (with-eval-after-load 'nndraft
    (setopt
     nndraft-directory
     (expand-file-name
      (convert-standard-filename "drafts/") gnus-home-directory)))

  (with-eval-after-load 'nnimap
    (setq nnimap-record-commands init-file-debug))

  (with-eval-after-load 'gnus-agent
    (setopt gnus-agent-synchronize-flags 'ask))

  (with-eval-after-load 'gnus-art       ; article
    (setopt
     gnus-buttonized-mime-types
     '("multipart/\\(signed\\|encrypted\\)")
     gnus-sorted-header-list
     '("^From:"
       "^X-RT-Originator"
       "^Newsgroups:"
       "^Subject:"
       "^Date:"
       "^Envelope-To:"
       "^Followup-To:"
       "^Reply-To:"
       "^Organization:"
       "^Summary:"
       "^Abstract:"
       "^Keywords:"
       "^To:"
       "^[BGF]?Cc:"
       "^Posted-To:"
       "^Mail-Copies-To:"
       "^Mail-Followup-To:"
       "^Apparently-To:"
       "^Resent-From:"
       "^User-Agent:"
       "^X-detected-operating-system:"
       "^X-Spam_action:"
       "^X-Spam_bar:"
       "^Message-ID:"
       ;; "^References:"
       "^List-Id:"
       "^Gnus-Warning:")
     gnus-visible-headers
     (mapconcat #'identity gnus-sorted-header-list "\\|")))

  ;; `gnus-dired'
  (with-eval-after-load 'dired
    (require 'gnus-dired)
    (add-hook 'dired-mode-hook #'gnus-dired-mode))

  (with-eval-after-load 'gnus-group
    (setopt
     gnus-permanently-visible-groups "\\(:INBOX$\\|:debian$\\)")
    (add-hook 'gnus-exit-gnus-hook #'quit-window)
    (add-hook 'gnus-group-mode-hook #'gnus-topic-mode)
    (add-hook 'gnus-group-mode-hook #'gnus-agent-mode))

  (with-eval-after-load 'gnus-msg
    (let ((bandali "Amin Bandali%s - https://www.shemshak.org/~amin"))
      (defvar bandali-csc-signature
        (mapconcat
         #'identity
         `(,(format bandali ", MMath")
           "Systems Committee <syscom@csclub.uwaterloo.ca>"
           "Computer Science Club of the University of Waterloo")
         "\n")))
    (setopt
     gnus-gcc-mark-as-read t
     gnus-message-replysign t
     gnus-posting-styles
     '((".*"
        (organization "sometimes"))
       ("nnimap\\+shemshak:.*"
        (address "amin@shemshak.org")
        ("X-Message-SMTP-Method" "smtp mail.shemshak.org 587")
        (gcc "nnimap+shemshak:INBOX"))
       ("nnimap\\+shemshak:debian.*"
        (address "bandali@debian.org")
        ("X-Message-SMTP-Method" "smtp mail-submit.debian.org 587")
        (gcc "nnimap+shemshak:debian"))
       ("nnimap\\+ia:.*"
        (address "bandali@archive.org")
        ("X-Message-SMTP-Method" "smtp smtp.gmail.com 587")
        (gcc "nnimap+ia:INBOX"))
       ("nnimap\\+debian:.*"
        (address "bandali@debian.org")
        ("X-Message-SMTP-Method" "smtp mail-submit.debian.org 587")
        (gcc "nnimap+debian:INBOX"))
       ("nnimap\\+gnu:.*"
        (address "bandali@gnu.org")
        ("X-Message-SMTP-Method" "smtp fencepost.gnu.org 587")
        (gcc "nnimap+gnu:INBOX"))
       ("nnimap\\+.*:l\\.ubuntu-.*"
        (address "bandali@ubuntu.com")
        ("X-Message-SMTP-Method" "smtp mail.shemshak.org 587"))
       ((header "list-id" ".*\\.lists.ubuntu.com")
        (address "bandali@ubuntu.com")
        ("X-Message-SMTP-Method" "smtp mail.shemshak.org 587"))
       ("nnimap\\+csc:.*"
        (address "bandali@csclub.uwaterloo.ca")
        ("X-Message-SMTP-Method" "smtp mail.csclub.uwaterloo.ca 587")
        (signature bandali-csc-signature)
        (gcc "nnimap+csc:Sent"))))

    ;; `gnus-registry'
    ;; (setopt
    ;;  gnus-registry-max-entries 2500
    ;;  gnus-registry-ignored-groups
    ;;  (append gnus-registry-ignored-groups
    ;;          '(("^nnimap:gnu\\.l" t) ("webmasters$" t))))
    ;; (require 'gnus-registry)
    ;; (gnus-registry-initialize)

    (with-eval-after-load 'gnus-search
      (setopt
       gnus-search-use-parsed-queries t))

    (with-eval-after-load 'gnus-start
      (setopt
       gnus-save-newsrc-file nil
       gnus-read-newsrc-file nil)
      (unless (fboundp 'gnus-notifications)
        (autoload #'gnus-notifications "gnus-start" nil t))
      (add-hook
       'gnus-after-getting-new-news-hook #'gnus-notifications))

    (with-eval-after-load 'gnus-sum       ; summary
      (setopt
       gnus-thread-sort-functions
       '(gnus-thread-sort-by-number
         gnus-thread-sort-by-subject
         gnus-thread-sort-by-date))

      (with-eval-after-load 'message
        (setopt
         gnus-ignored-from-addresses message-dont-reply-to-names))

      (defun bandali-gnus-junk-article (&optional n)
        (interactive "P" gnus-summary-mode)
        (gnus-summary-move-article
         n
         (gnus-group-prefixed-name
          "Junk"
          (gnus-find-method-for-group gnus-newsgroup-name))))

      (defvar bandali-gnus-summary-prefix-map)
      (define-prefix-command 'bandali-gnus-summary-prefix-map)
      (bandali-define-key gnus-summary-mode-map
        "v" 'bandali-gnus-summary-prefix-map)
      (bandali-define-key bandali-gnus-summary-prefix-map
        "r r" #'gnus-summary-very-wide-reply
        "r q" #'gnus-summary-very-wide-reply-with-original
        "R r" #'gnus-summary-reply
        "R q" #'gnus-summary-reply-with-original
        "r a w" #'gnus-summary-show-raw-article
        "s" #'bandali-gnus-junk-article))

    (with-eval-after-load 'gnus-topic
      (setopt
       ;; gnus-topic-line-format "%i[ %A: %(%{%n%}%) ]%v\n"
       gnus-topic-line-format "%i[ %(%{%n%}%) (%A) ]%v\n")
      (setq
       gnus-topic-topology
       `(("Gnus" visible nil nil)
         (("misc" visible nil nil))
         (("csc" visible nil nil))
         (("ia" visible nil nil))
         (("shemshak" visible nil nil))
         (("debian" visible nil nil))
         (("gnu" visible nil nil))
         ;; (("old-gnu" visible nil nil))
         )))

    ;; (with-eval-after-load 'gnus-win
    ;;   (setopt gnus-use-full-window nil))

    (with-eval-after-load 'mm-archive
      (add-to-list
       'mm-archive-decoders
       '("application/gzip" nil "gunzip" "-S" ".zip" "-kd" "%f" "-r")))

    (with-eval-after-load 'mm-decode
      (setopt
       ;; mm-attachment-override-types `("text/x-diff" "text/x-patch"
       ;;                                ,@mm-attachment-override-types)
       mm-discouraged-alternatives '("text/html" "text/richtext")
       mm-decrypt-option 'known
       mm-verify-option 'known)
      (add-to-list
       'mm-inline-media-tests
       `("application/gzip" mm-archive-dissect-and-inline identity))
      (add-to-list 'mm-inlined-types "application/gzip" 'append))

    (with-eval-after-load 'mm-uu
      (when (version< "27" emacs-version)
        (set-face-attribute 'mm-uu-extract nil :extend t)))

    (with-eval-after-load 'mml
      (setopt
       mml-attach-file-at-the-end t
       mml-content-disposition-alist
       '((text
          (markdown . "attachment")
          (rtf . "attachment")
          (t . "inline"))
         (t . "attachment"))))

    (with-eval-after-load 'mml-sec
      (setopt
       mml-secure-openpgp-encrypt-to-self t
       mml-secure-openpgp-sign-with-sender t))))
(bandali-define-key global-map
  "C-c g" #'gnus-plugged
  "C-c G" #'gnus-unplugged
  "C-c m" (lambda () (interactive) (async-shell-command "m")))

(define-advice gnus (:around (orig &rest args) bandali)
  "So we can match on Gnus's *Group* buffer in `display-buffer-alist'."
  (let ((switch-to-buffer-obey-display-actions t))
    (apply orig args)))

(with-eval-after-load 'message
  ;; Redefine for a simplified In-Reply-To header
  ;; (https://todo.sr.ht/~sircmpwn/lists.sr.ht/67)
  (defun message-make-in-reply-to ()
    "Return the In-Reply-To header for this message."
    (when message-reply-headers
      (let ((from (mail-header-from message-reply-headers))
            (msg-id (mail-header-id message-reply-headers)))
        (when from
          msg-id))))

  (setopt
   message-elide-ellipsis "> [... %l lines elided]\n"
   message-citation-line-format "%N wrote:\n"
   message-citation-line-function
   #'message-insert-formatted-citation-line
   message-confirm-send t
   message-fill-column 70
   message-forward-as-mime t
   ;; message-kill-buffer-on-exit t
   message-send-mail-function #'smtpmail-send-it
   message-subscribed-address-functions
   '(gnus-find-subscribed-addresses)
   message-dont-reply-to-names
   (mapconcat
    #'identity
    '("amin@shemshak\\.org"
      "bandali@kelar\\.org"
      "\\(bandali\\|mab\\|aminb?\\)@gnu\\.org"
      "a?bandali@\\(csclub\\.\\)?uwaterloo\\.ca"
      "bandali@gnu\\.ca"
      "bandali@ubuntu\\.com"
      "bandali@debian\\.org"
      "bandali@archive\\.org")
    "\\|"))

  (defun bandali-newlines-or-asterism (arg)
    "Create newlines per my liking, or insert asterism if ARG is
non-nil."
    (interactive "P")
    (if arg
        (bandali-insert-asterism)
      (progn
        (delete-region (line-beginning-position) (line-end-position))
        (newline)
        (open-line 1))))
  (bandali-define-key message-mode-map
    "M-RET" #'bandali-newlines-or-asterism
    "C-c C-s" nil
    ;; breaks C-S-n selection
    ;; "<remap> <next-line>" #'mail-abbrev-next-line
    "<remap> <end-of-buffer>" #'mail-abbrev-end-of-buffer))
(add-hook 'message-mode-hook #'flyspell-mode)

;; (with-eval-after-load 'sendmail
;;   (setopt mail-header-separator ""))
;; (with-eval-after-load 'smtpmail
;;   (setopt smtpmail-queue-mail t
;;           smtpmail-queue-dir (concat bandali-maildir "queue/")))

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


;;;; ERC

(with-eval-after-load 'erc
  (setopt
   erc-auto-query 'bury
   erc-autojoin-domain-only nil
   erc-dcc-get-default-directory (locate-user-emacs-file "erc-dcc")
   erc-email-userid "bandali"
   ;; erc-lurker-hide-list '("JOIN" "PART" "QUIT")
   erc-nick "bandali"
   erc-prompt "erc>"
   erc-prompt-for-password nil
   erc-query-display 'buffer
   ;; erc-server-reconnect-attempts 5
   erc-server-reconnect-timeout 3
   erc-show-speaker-membership-status t)

  (add-to-list 'erc-modules 'keep-place)
  (when (display-graphic-p)
    (add-to-list 'erc-modules 'notifications)
    (add-to-list 'erc-modules 'smiley))
  (add-to-list 'erc-modules 'spelling)

  (declare-function erc-update-modules "erc")
  (erc-update-modules)

  (with-eval-after-load 'erc-match
    (setopt
     erc-pal-highlight-type 'nick
     erc-pals
     '("corwin" "sachac" "^rwp" "^iank" "thomzane"
       "neverwas" "^gopar" "technomancy"))
    (set-face-attribute
     'erc-pal-face nil
     :foreground 'unspecified
     :weight 'unspecified
     :inherit 'erc-nick-default-face
     :background "#ffffdf"))

  (with-eval-after-load 'erc-pcomplete
    (setopt erc-pcomplete-nick-postfix ","))

  (with-eval-after-load 'erc-stamp
    (setopt
     erc-timestamp-only-if-changed-flag nil
     erc-timestamp-format "%T "
     erc-insert-timestamp-function #'erc-insert-timestamp-left)
    (set-face-attribute
     'erc-timestamp-face nil
     :foreground "#aaaaaa"
     :weight 'unspecified
     :background 'unspecified))

  (with-eval-after-load 'erc-track
    (setopt
     erc-track-enable-keybindings nil
     erc-track-exclude-types
     '("JOIN" "MODE" "NICK" "PART" "QUIT"
       "324" "329" "332" "333" "353" "477")
     erc-track-position-in-mode-line t
     erc-track-priority-faces-only 'all
     erc-track-shorten-function nil
     erc-track-showcount t))

  (bandali-define-key global-map
    "C-c w e" #'erc-switch-to-buffer-other-window)
  (bandali-define-key erc-mode-map
    "M-a" #'erc-track-switch-buffer))
(bandali-define-key global-map
  "C-c e l"
  (lambda ()
    (interactive)
    (erc-tls
     :id "bnc-libera"
     :server "bnc.shemshak.org"
     :port 6697
     :user "bandali/irc.libera.chat"))
  "C-c e o"
  (lambda ()
    (interactive)
    (erc-tls
     :id "bnc-oftc"
     :server "bnc.shemshak.org"
     :port 6697
     :user "bandali/irc.oftc.net")))


;;;; Web

(with-eval-after-load 'shr
  (setopt shr-max-width 70))

(with-eval-after-load 'eww
  (setopt
   eww-download-directory
   (file-name-as-directory (getenv "XDG_DOWNLOAD_DIR"))))

(with-eval-after-load 'elpher
  (setopt elpher-gemini-max-fill-width 70))
