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

;; Copyright (c) 2018-2024 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:

;; bandali's opinionated GNU Emacs configs.  I tend to use the latest
;; development trunk of emacs.git, but I try to maintain backward
;; compatibility with a few of the recent older GNU Emacs releases
;; so I could easily reuse it on machines stuck with older Emacsen.

;; When initially putting this together, I took inspiration from
;; configurations of many great people.  Some that I could remember
;; off the top of my head are:
;;
;; - https://github.com/dieggsy/dotfiles
;; - https://github.com/dakra/dmacs
;; - https://pages.sachachua.com/.emacs.d/Sacha.html
;; - https://github.com/dakrone/eos
;; - https://cce.whatthefuck.computer/
;; - https://github.com/jwiegley/dot-emacs
;; - https://github.com/wasamasa/dotemacs
;; - https://github.com/hlissner/doom-emacs

;;; Code:

;;; Emacs initialization

;; 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.
(defconst b/gc-cons-threshold gc-cons-threshold)
(defconst 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)

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

  (require 'package)
  (package-initialize))
(add-hook 'after-init-hook #'b/post-init)

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


;;; Initial setup

(eval-and-compile
  (defsubst b/emacs.d (path)
    "Expand path PATH relative to `user-emacs-directory'."
    (expand-file-name
     (convert-standard-filename path) user-emacs-directory))

  ;; Wrappers around the new keybinding functions, with fallback to
  ;; the corresponding older lower level function on older Emacsen.
  (defsubst b/keymap-set (keymap key definition)
    (if (version< emacs-version "29")
        (define-key keymap (kbd key) definition)
      (keymap-set keymap key definition)))
  (defsubst b/keymap-global-set (key command)
    (if (version< emacs-version "29")
        (global-set-key (kbd key) command)
      (keymap-global-set key command)))
  (defsubst b/keymap-local-set (key command)
    (if (version< emacs-version "29")
        (local-set-key (kbd key) command)
      (keymap-local-set key command)))
  (defsubst b/keymap-global-unset (key)
    (if (version< emacs-version "29")
        (global-unset-key (kbd key))
      (keymap-global-unset key 'remove)))
  (defsubst b/keymap-local-unset (key)
    (if (version< emacs-version "29")
        (local-unset-key (kbd key))
      (keymap-local-unset key 'remove)))

  (when (version< emacs-version "29")
    ;; Emacs 29 introduced the handy `setopt' macro for setting user
    ;; options (defined with `defcustom') with a syntax similar to
    ;; `setq'.  So, we define it on older Emacsen that don't have it.
    (defmacro setopt (&rest pairs)
      "Set VARIABLE/VALUE pairs, and return the final VALUE.
This is like `setq', but is meant for user options instead of
plain variables.  This means that `setopt' will execute any
`custom-set' form associated with VARIABLE.

\(fn [VARIABLE VALUE]...)"
      (declare (debug setq))
      (unless (zerop (mod (length pairs) 2))
        (error "PAIRS must have an even number of variable/value members"))
      (let ((expr nil))
        (while pairs
          (unless (symbolp (car pairs))
            (error "Attempting to set a non-symbol: %s" (car pairs)))
          (push `(setopt--set ',(car pairs) ,(cadr pairs))
                expr)
          (setq pairs (cddr pairs)))
        (macroexp-progn (nreverse expr))))

    (defun setopt--set (variable value)
      (custom-load-symbol variable)
      ;; Check that the type is correct.
      (when-let ((type (get variable 'custom-type)))
        (unless (widget-apply (widget-convert type) :match value)
          (warn "Value `%S' does not match type %s" value type)))
      (put variable 'custom-check-value (list value))
      (funcall (or (get variable 'custom-set) #'set-default) variable value))))

;; Separate custom file (don't want it mixing with init.el).
(setopt custom-file (b/emacs.d "custom.el"))
(with-eval-after-load 'custom
  (when (file-exists-p custom-file)
    (load custom-file)))

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


;;; Defaults

;;;; C source code

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

(setq
 ;; line-spacing 3
 completion-ignore-case t
 read-buffer-completion-ignore-case t
 enable-recursive-minibuffers t
 resize-mini-windows t
 message-log-max 20000
 mode-line-compact t
 ;; mouse-autoselect-window t
 scroll-conservatively 15
 scroll-preserve-screen-position 1
 ;; I don't feel like randomly jumping out of my chair.
 ring-bell-function 'ignore)

;;;; elisp source code

(with-eval-after-load 'minibuffer
  (setopt read-file-name-completion-ignore-case t))

(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)
  (add-to-list 'auto-mode-alist '("\\README.*" . text-mode))
  (add-to-list 'auto-mode-alist '("\\.*rc$" . conf-mode))
  (add-to-list 'auto-mode-alist '("\\.bashrc$" . sh-mode)))

(setq disabled-command-function nil)

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

(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 %Y-%m-%d %-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
   `(,@zoneinfo-style-world-list
     ("Etc/UTC" "UTC")
     ("Asia/Tehran" "Tehran")
     ("Australia/Melbourne" "Melbourne")))
  (unless (display-graphic-p)
    (display-time-mode)))

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

(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)
  (b/keymap-global-set "M-H" #'windmove-left)
  (b/keymap-global-set "M-L" #'windmove-right)
  (b/keymap-global-set "M-K" #'windmove-up)
  (b/keymap-global-set "M-J" #'windmove-down))

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

(b/keymap-global-set "C-x v C-=" #'vc-ediff)

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

(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 'face-remap
;;   (setopt
;;    ;; Gentler font resizing.
;;    text-scale-mode-step 1.05))

(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

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

(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 'auth-source
;;   (setopt
;;    auth-sources '("~/.authinfo.gpg")
;;    authinfo-hidden
;;    (regexp-opt '("password" "client-secret" "token"))))

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

(when (display-graphic-p)
  (set-fontset-font t 'arabic "Sahel WOL")
  (with-eval-after-load 'faces
    (let ((grey "#e7e7e7"))
      (set-face-attribute 'default nil
                          :font "Source Code Pro"
                          :weight 'medium)
      (set-face-attribute 'fixed-pitch nil
                          :font "Source Code Pro"
                          :weight 'medium)
      (set-face-attribute 'mode-line nil
                          :background grey
                          :inherit 'fixed-pitch))))

(when (and (version< emacs-version "28") mode-line-compact)
  ;; Manually make some `mode-line' spaces smaller.
  ;; Emacs 28 and above do a terrific job at this out of the box
  ;; when `mode-line-compact' is set to t (see above)."
  (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")))


;;; Useful utilities

(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/join-line-top ()
  "Like `join-line', but join next line to the current line."
  (interactive)
  (join-line 1))

(defun b/*scratch* ()
  "Switch to `*scratch*' buffer, creating it if it does not exist."
  (interactive)
  (let ((fun (if (functionp #'get-scratch-buffer-create)
                 #'get-scratch-buffer-create ; (version<= "29" emacs-version)
               #'startup--get-buffer-create-scratch))) ; (version< emacs-version "29")
    (switch-to-buffer (funcall fun))))

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

(defun b/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))))


;;; General key bindings

(let ((kfs
       '(("C-c i"   . ielm)
         ("C-c d"   . b/duplicate-line-or-region)
         ("C-c j"   . b/join-line-top)
         ("C-S-j"   . b/join-line-top)
         ("C-c s c" . b/*scratch*)
         ("C-c v"   . b/invert-default-face)
         ;; evaling and macro-expanding
         ("C-c e b" . eval-buffer)
         ("C-c e e" . eval-last-sexp)
         ("C-c e m" . pp-macroexpand-last-sexp)
         ("C-c e r" . eval-region)
         ;; emacs things
         ("C-c e i" . emacs-init-time)
         ("C-c e u" . emacs-uptime)
         ("C-c e v" . emacs-version)
         ;; finding
         ("C-c f ." . find-file)
         ("C-c f l" . find-library)
         ("C-c f p" . find-file-at-point)
         ;; frames
         ("C-c F m" . make-frame-command)
         ("C-c F d" . delete-frame)
         ;; help/describe
         ("C-c h F" . describe-face))))
  (dolist (kf kfs)
    (let ((key (car kf))
          (fun (cdr kf)))
      (b/keymap-global-set key fun))))

(when (display-graphic-p)
  ;; Too easy to accidentally suspend (freeze) Emacs GUI.
  (b/keymap-global-unset "C-z"))


;;; Essential packages

(add-to-list 'load-path (b/emacs.d "lisp"))

(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) '("adelita")))
  (add-to-list 'load-path (b/emacs.d "lisp/xelb"))
  (add-to-list 'load-path (b/emacs.d "lisp/exwm"))
  (require 'exwm)

  (global-set-key (kbd "C-x b") #'exwm-workspace-switch-to-buffer)

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

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

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

  (setq
   ;; Initial number of workspaces
   exwm-workspace-number 4
   ;; Global keybindings
   exwm-input-global-keys
   `(([?\s-r] . exwm-reset) ; Reset (to line-mode)
     ([?\s-/] . exwm-workspace-switch)
     ([?\s-\s] . (lambda (command)
                   (interactive (list (read-shell-command "$ ")))
                   (start-process-shell-command command nil command)))
     ([?\s-\\] . (lambda ()
                   (interactive)
                   (start-process-shell-command
                    "passmenu" nil "passmenu --type")))
     ([s-return] . (lambda ()
                     (interactive)
                     (start-process "" nil "xterm")))
     ([S-s-return] . (lambda ()
                       (interactive)
                       (start-process "" nil "xterm"
                                      "-name" "floating")))
     ([?\s-h] . windmove-left)
     ([?\s-j] . windmove-down)
     ([?\s-k] . windmove-up)
     ([?\s-l] . windmove-right)
     ([?\s-H] . windmove-swap-states-left)
     ([?\s-J] . windmove-swap-states-down)
     ([?\s-K] . windmove-swap-states-up)
     ([?\s-L] . windmove-swap-states-right)
     ([?\M-\s-h] . shrink-window-horizontally)
     ([?\M-\s-l] . enlarge-window-horizontally)
     ([?\M-\s-k] . shrink-window)
     ([?\M-\s-j] . enlarge-window)
     ([mode-line mouse-4] . b/exwm-ws-prev) ; up
     ([mode-line mouse-5] . b/exwm-ws-next) ; down
     ([mode-line mouse-6] . b/exwm-ws-prev) ; left
     ([mode-line mouse-7] . b/exwm-ws-next) ; right
     ([?\s-\[] . b/exwm-ws-prev)
     ([?\s-\]] . b/exwm-ws-next)
     ([?\s-{] . (lambda ()
                  (interactive)
                  (exwm-workspace-move-window
                   (b/exwm-ws-prev-index))))
     ([?\s-}] . (lambda ()
                  (interactive)
                  (exwm-workspace-move-window
                   (b/exwm-ws-next-index))))
     ,@(mapcar (lambda (i)
                 `(,(kbd (format "s-%d" i)) .
                   (lambda ()
                     (interactive)
                     (exwm-workspace-switch-create ,i))))
               (number-sequence 0 (1- exwm-workspace-number)))
     ,@(mapcar
        (lambda (i)
          `(,(kbd (format "s-%s"
                          (plist-get b/shifted-ws-names i)))
            .
            (lambda ()
              (interactive)
              (exwm-workspace-move-window ,i))))
        (number-sequence 0 (1- exwm-workspace-number)))
     ([?\s-.] . exwm-floating-toggle-floating)
     ([?\s-f] . exwm-layout-toggle-fullscreen)
     ([?\s-W] . (lambda ()
                  (interactive)
                  (kill-buffer (current-buffer))))
     ([?\s-Q] . (lambda ()
                  (interactive)
                  (exwm-manage--kill-client)))
     ([?\s-\'] . (lambda ()
                   (interactive)
                   (start-process-shell-command
                    "dmneu-light" nil "dmenu-light")))
     ([\s-XF86Back] . previous-buffer)
     ([\s-XF86Forward] . next-buffer))
   ;; 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 delete])
     ([?\M-<] . C-home)
     ([?\M->] . C-end)
     ;; cut/copy/paste
     ([?\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-g] . [?\C-g])
     ([?\s-s] . [?\C-s])
     ([?\C-g] . [escape])
     ([?\C-/] . [?\C-z])))

  (with-eval-after-load 'exwm-manage
  (setq
   exwm-manage-configurations
   '(((equal exwm-instance-name "floating")
      floating t
      floating-mode-line nil)))
  (add-hook
   'exwm-manage-finish-hook
   (lambda ()
     (when exwm-class-name
       (cond
        ((member exwm-class-name '("Abrowser" "IceCat" "Firefox-esr"))
         (exwm-input-set-local-simulation-keys
          `(,@exwm-input-simulation-keys
            ([?\C-\S-d] . [?\C-d]))))
        ((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-enable)
  (add-hook 'exwm-update-class-hook #'b/exwm-rename-buffer)
  (add-hook 'exwm-update-title-hook #'b/exwm-rename-buffer)

  (defun b/fix-ido-buffer-window-other-frame ()
    "Fix `ido-buffer-window-other-frame'."
    (defalias 'b/ido-buffer-window-other-frame-orig
      (symbol-function 'ido-buffer-window-other-frame))
    (defun ido-buffer-window-other-frame (buffer)
      "This is a version redefined for EXWM.

The original one is at `b/ido-buffer-window-other-frame-orig'."
      (with-current-buffer (window-buffer (selected-window))
        (if (and (derived-mode-p 'exwm-mode)
                 exwm--floating-frame)
            ;; Switch from a floating frame.
            (with-current-buffer buffer
              (if (and (derived-mode-p 'exwm-mode)
                       exwm--floating-frame
                       (eq exwm--frame exwm-workspace--current))
                  ;; Switch to another floating frame.
                  (frame-root-window exwm--floating-frame)
                ;; Do not switch if the buffer is not on the current workspace.
                (or (get-buffer-window buffer exwm-workspace--current)
                    (selected-window))))
          (with-current-buffer buffer
            (when (derived-mode-p 'exwm-mode)
              (if (eq exwm--frame exwm-workspace--current)
                  (when exwm--floating-frame
                    ;; Switch to a floating frame on the current workspace.
                    (frame-selected-window exwm--floating-frame))
                ;; Do not switch to exwm-mode buffers on other workspace (which
                ;; won't work unless `exwm-layout-show-all-buffers' is set)
                (unless exwm-layout-show-all-buffers
                  (selected-window)))))))))
  (add-hook 'exwm-init-hook #'b/fix-ido-buffer-window-other-frame)

  (require 'exwm-input)
  (defun b/exwm-ws-prev-index ()
    "Return the index for the previous EXWM workspace, wrapping
around if needed."
    (if (= exwm-workspace-current-index 0)
        (1- exwm-workspace-number)
      (1- exwm-workspace-current-index)))

  (defun b/exwm-ws-next-index ()
    "Return the index for the next EXWM workspace, wrapping
around if needed."
    (if (= exwm-workspace-current-index
           (1- exwm-workspace-number))
        0
      (1+ exwm-workspace-current-index)))

  (defun b/exwm-ws-prev ()
    "Switch to previous EXWM workspace, wrapping around if needed."
    (interactive)
    (exwm-workspace-switch-create
     (b/exwm-ws-prev-index)))

  (defun b/exwm-ws-next ()
    "Switch to next EXWM workspace, wrapping around if needed."
    (interactive)
    (exwm-workspace-switch-create
     (b/exwm-ws-next-index)))

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

  ;; Scroll up/down/left/right on the echo area
  (define-key minibuffer-inactive-mode-map [mouse-4] #'b/exwm-ws-prev)
  (define-key minibuffer-inactive-mode-map [mouse-5] #'b/exwm-ws-next)
  (define-key minibuffer-inactive-mode-map [mouse-6] #'b/exwm-ws-prev)
  (define-key minibuffer-inactive-mode-map [mouse-7] #'b/exwm-ws-next)

  ;; (require 'exwm-randr)
  ;; (setq
  ;;  exwm-randr-workspace-monitor-plist
  ;;  '(0 "eDP-1"
  ;;      1 "eDP-1" 2 "eDP-1" 3 "eDP-1"
  ;;      4 "eDP-1" 5 "eDP-1" 6 "eDP-1"
  ;;      7 "HDMI-1" 8 "HDMI-1" 9 "HDMI-1"))
  ;; ;; (add-hook
  ;; ;;  'exwm-randr-screen-change-hook
  ;; ;;  (lambda ()
  ;; ;;    (start-process-shell-command
  ;; ;;     "xrandr" nil
  ;; ;;     "xrandr --output HDMI-1 --mode 1280x720 --above eDP-1 --auto")))
  ;; (exwm-randr-enable)

  (require 'exwm-systemtray)
  (exwm-systemtray-enable)

  ;; (add-to-list 'load-path (b/lisp "exwm-edit"))
  ;; (require 'exwm-edit)

  (with-eval-after-load 'exwm-workspace
    (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 'exwm-layout
    (setq exwm-layout-show-all-buffers t)))

;; recently opened files
(run-with-idle-timer 0.2 nil #'require 'recentf)
(with-eval-after-load 'recentf
  (setopt recentf-max-saved-items 2000)
  (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)))
  (b/keymap-global-set "C-c f r" #'b/recentf-open))

(with-eval-after-load 'eshell
  (setopt
   eshell-hist-ignoredups t
   eshell-input-filter #'eshell-input-filter-initial-space
   eshell-prompt-regexp "^[^#$\n]* [#$] ; "
   eshell-prompt-function
   (lambda ()
     (let ((uid (if (functionp #'file-user-uid)
                    #'file-user-uid   ; (version<= "30" emacs-version)
                  #'user-uid)))       ; (version< emacs-version "30")
       (concat ": "
               (system-name)
               ":"
               (abbreviate-file-name (eshell/pwd))
               (unless (eshell-exit-success-p)
                 (format " [%d]" eshell-last-command-status))
               (if (= (funcall uid) 0) " # " " $ ")
               "; "))))
  (eval-when-compile
    (defvar eshell-prompt-regexp)
    (declare-function eshell-life-is-too-much "esh-mode")
    (declare-function eshell-send-input "esh-mode"
                      (&optional use-region queue-p no-newline)))
  (defun b/eshell-quit-or-delete-char (arg)
    (interactive "p")
    (if (and (eolp) (looking-back eshell-prompt-regexp nil))
        (eshell-life-is-too-much)
      (delete-char arg)))
  (defun b/eshell-clear ()
    (interactive)
    (let ((inhibit-read-only t))
      (erase-buffer))
    (eshell-send-input))
  (defun b/eshell-history ()
    (interactive)
    (completing-read "Eshell history: "
                     (ring-elements eshell-history-ring)))
  (with-eval-after-load 'esh-mode
    (let ((m eshell-mode-map))
      (b/keymap-set m "C-d" #'b/eshell-quit-or-delete-char)
      (b/keymap-set m "C-S-l" #'b/eshell-clear)))
  (with-eval-after-load 'esh-hist
    (let ((m eshell-hist-mode-map))
      (b/keymap-set m "M-r" #'b/eshell-history))))
(b/keymap-global-set "C-c s e" #'eshell)

(with-eval-after-load 'ibuffer
  (setopt
   ibuffer-saved-filter-groups
   '(("default"
      ("dired" (mode . dired-mode))
      ("erc" (mode . erc-mode))
      ("gnus"
       (or
        (mode . gnus-group-mode)
        (mode . gnus-server-mode)
        (mode . gnus-summary-mode)
        (mode . gnus-article-mode)
        (mode . message-mode)))
      ("shell"
       (or
        (mode . eshell-mode)
        (mode . shell-mode)
        (mode . term-mode)))
      ("tex"
       (or
        (mode . tex-mode)
        (mode . bibtex-mode)
        (mode . latex-mode)))))
   ibuffer-formats
   `((mark modified read-only locked
           " " (name 18 18 :left :elide)
           " " (size-h 9 -1 :right)
           " " (mode 16 16 :left :elide) " " filename-and-process)
     ,@ibuffer-formats))
  ;; Use human readable Size column instead of original one
  (define-ibuffer-column size-h
    (:name "Size" :inline t)
    (cond
     ((> (buffer-size) (* 1024 1024))
      (format "%7.1fM" (/ (buffer-size) (* 1024.0 1024.0))))
     ((> (buffer-size) (* 100 1024))
      (format "%7.0fK" (/ (buffer-size) 1024.0)))
     ((> (buffer-size) 1024)
      (format "%7.1fK" (/ (buffer-size) 1024.0)))
     (t (format "%8d" (buffer-size)))))

  (let ((m ibuffer-mode-map))
    (b/keymap-set m "P" #'ibuffer-backward-filter-group)
    (b/keymap-set m "N" #'ibuffer-forward-filter-group)
    (b/keymap-set m "M-p" #'ibuffer-do-print)
    (b/keymap-set m "M-n" #'ibuffer-do-shell-command-pipe-replace)))
(b/keymap-global-set "C-x C-b" #'ibuffer)
(declare-function
 ibuffer-switch-to-saved-filter-groups "ibuf-ext" (name))
(add-hook
 'ibuffer-hook
 (lambda () (ibuffer-switch-to-saved-filter-groups "default")))

(with-eval-after-load 'dired
  ;; (require 'ls-lisp)
  (setopt
   dired-dwim-target t
   ;; dired-listing-switches "-alh --group-directories-first"
   dired-listing-switches "-alh"
   ;; ls-lisp-dirs-first t
   ls-lisp-use-insert-directory-program nil)

  (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
  (let ((m dired-mode-map))
    (b/keymap-set m "b" #'dired-up-directory)
    (b/keymap-set m "E" #'dired-ediff-files)
    (b/keymap-set m "e" #'dired-toggle-read-only)
    (b/keymap-set m "\\" #'dired-hide-details-mode))

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

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

(with-eval-after-load 'help-mode
  (let ((m help-mode-map))
    (b/keymap-set m "p" #'backward-button)
    (b/keymap-set m "n" #'forward-button)
    (b/keymap-set m "b" #'help-go-back)
    (b/keymap-set m "f" #'help-go-forward)))

(with-eval-after-load 'doc-view
  (b/keymap-set doc-view-mode-map "M-RET" #'image-previous-line))

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

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


;;; Email

(defvar b/maildir
  (expand-file-name (convert-standard-filename "~/mail/")))
(with-eval-after-load 'recentf
  (add-to-list 'recentf-exclude b/maildir))

(setopt
 mail-user-agent 'gnus-user-agent
 read-mail-command #'gnus)

(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
  (with-eval-after-load 'nnimap
    (setq nnimap-record-commands init-file-debug))

  (setopt
   gnus-select-method '(nnnil "")
   gnus-secondary-select-methods
   `(,@(when (member (system-name) '("darya" "nostalgia" "selene"))
         '((nnimap
            "canonical"
            (nnimap-stream plain)
            (nnimap-address "127.0.0.1")
            (nnimap-server-port 143)
            (nnimap-authenticator plain)
            (nnimap-user "bandali@canonical.local"))))
     (nnimap
      "kelar"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "bandali@kelar.local")
      ;; (nnmail-expiry-wait immediate)
      (nnmail-expiry-target nnmail-fancy-expiry-target)
      (nnmail-fancy-expiry-targets
       (("from" ".*" "nnimap+kelar:Archive.%Y"))))
     (nnimap
      "shemshak"
      (nnimap-stream plain)
      (nnimap-address "127.0.0.1")
      (nnimap-server-port 143)
      (nnimap-authenticator plain)
      (nnimap-user "bandali@shemshak.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
        (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")))
     ;; (nnimap
     ;;  "sfl"
     ;;  (nnimap-stream plain)
     ;;  (nnimap-address "127.0.0.1")
     ;;  (nnimap-server-port 143)
     ;;  (nnimap-authenticator plain)
     ;;  (nnimap-user "amin.bandali@savoirfairelinux.local"))
     )
   gnus-message-archive-group "nnimap+kelar: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 (b/emacs.d "gnus/")
   gnus-directory
   (expand-file-name
    (convert-standard-filename "news/") gnus-home-directory)
   gnus-interactive-exit nil
   gnus-user-agent '(emacs gnus type))

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

  (when (version< emacs-version "27")
    (with-eval-after-load 'nnmail
      (add-to-list
       'nnmail-split-abbrev-alist
       '(list . "list-id\\|list-post\\|x-mailing-list\\|x-beenthere\\|x-loop")
       'append)))

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

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

  (with-eval-after-load 'gnus-group
    (setopt
     gnus-permanently-visible-groups "\\(:INBOX$\\|:gnu$\\)")
    (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://kelar.org/~bandali"))
      (defvar b/csc-signature
        (mapconcat
         #'identity
         `(,(format bandali ", MMath")
           "Systems Committee <syscom@csclub.uwaterloo.ca>"
           "Computer Science Club of the University of Waterloo")
         "\n"))
      (defvar b/sfl-signature
        (mapconcat
         #'identity
         `(,(format bandali "")
           "Volunteer, Savoir-faire Linux"
           "jami:bandali")
         "\n")))
    (setopt
     gnus-gcc-mark-as-read t
     gnus-message-replysign t
     gnus-posting-styles
     '(("nnimap\\+kelar:.*"
        (address "bandali@kelar.org")
        ("X-Message-SMTP-Method" "smtp mail.kelar.org 587")
        (gcc "nnimap+kelar:INBOX"))
       ("nnimap\\+shemshak:.*"
        (address "amin@shemshak.org")
        ("X-Message-SMTP-Method" "smtp mail.shemshak.org 587")
        (gcc "nnimap+shemshak:Sent"))
       ("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\\+canonical:.*"
        (address "bandali@canonical.com")
        ("X-Message-SMTP-Method" "smtp smtp.canonical.com 587")
        (signature nil)
        (gcc "nnimap+canonical:Sent"))
       ((header "to" "amin\\.bandali@canonical\\.com")
        (address "amin.bandali@canonical.com"))
       ((header "cc" "amin\\.bandali@canonical\\.com")
        (address "amin.bandali@canonical.com"))
       ;; ("nnimap\\+.*:l\\.ubuntu-.*"
       ;;  (address "bandali@ubuntu.com")
       ;;  ("X-Message-SMTP-Method" "smtp mail.kelar.org 587"))
       ;; ((header "list-id" ".*\\.lists.ubuntu.com")
       ;;  (address "bandali@ubuntu.com")
       ;;  ("X-Message-SMTP-Method" "smtp mail.kelar.org 587"))
       ("nnimap\\+csc:.*"
        (address "bandali@csclub.uwaterloo.ca")
        ("X-Message-SMTP-Method" "smtp mail.csclub.uwaterloo.ca 587")
        (signature b/csc-signature)
        (gcc "nnimap+csc:Sent"))
       ("nnimap\\+sfl:.*"
        (address "amin.bandali@savoirfairelinux.com")
        ("X-Message-SMTP-Method" "smtp mail.savoirfairelinux.com 587")
        (signature b/sfl-signature)
        (gcc "nnimap+sfl:Sent")))))

  ;; (require 'gnus-registry)
  ;; (with-eval-after-load 'gnus-registry
  ;;   (setopt
  ;;    gnus-registry-max-entries 2500
  ;;    gnus-registry-ignored-groups
  ;;    (append gnus-registry-ignored-groups
  ;;            '(("^nnimap:gnu\\.l" t) ("webmasters$" t))))
  ;;   (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)
    (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 b/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 b/gnus-summary-prefix-map)
    (define-prefix-command 'b/gnus-summary-prefix-map)
    (b/keymap-set
     gnus-summary-mode-map "v" 'b/gnus-summary-prefix-map)
    (let ((m b/gnus-summary-prefix-map))
      (b/keymap-set m "r r" #'gnus-summary-very-wide-reply)
      (b/keymap-set m "r q" #'gnus-summary-very-wide-reply-with-original)
      (b/keymap-set m "R r" #'gnus-summary-reply)
      (b/keymap-set m "R q" #'gnus-summary-reply-with-original)
      (b/keymap-set m "r a w" #'gnus-summary-show-raw-article)
      (b/keymap-set m "s" #'b/gnus-junk-article)))

  (with-eval-after-load 'gnus-topic
    ;; (setopt gnus-topic-line-format "%i[ %A: %(%{%n%}%) ]%v\n")
    (setopt gnus-topic-line-format "%i[ %(%{%n%}%) (%A) ]%v\n")
    (setq gnus-topic-topology
     `(("Gnus" visible nil nil)
       (("misc" visible nil nil))
       ,@(when (member (system-name) '("darya" "nostalgia" "selene"))
           '((("canonical" visible nil nil))))
       (("csc" visible nil nil))
       (("kelar" visible nil nil))
       (("shemshak" visible nil nil))
       (("debian" visible nil nil))
       (("gnu" visible nil nil))
       ;; (("old-gnu" visible nil nil))
       ;; (("sfl" 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))
    (when (version< emacs-version "27")
      (setopt mm-uu-diff-groups-regexp ".")))

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

  (with-eval-after-load 'recentf
    (add-to-list 'recentf-exclude gnus-home-directory)))
(b/keymap-global-set "C-c g" #'gnus-plugged)
(b/keymap-global-set "C-c G" #'gnus-unplugged)

(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 "[...]\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
    '("bandali@kelar\\.org"
      "amin@shemshak\\.org"
      "\\(bandali\\|mab\\|aminb?\\)@gnu\\.org"
      "a?bandali@\\(csclub\\.\\)?uwaterloo\\.ca"
      "amin\\.bandali@savoirfairelinux\\.com"
      "\\(amin\\.\\)?bandali@canonical\\.com"
      "bandali@ubuntu\\.com"
      "bandali@debian\\.org")
    "\\|"))

  (defun b/newlines-or-asterism (arg)
    "Create newlines per my liking, or insert asterism if ARG is
non-nil."
    (interactive "P")
    (if arg
        (b/insert-asterism)
      (progn
        (beginning-of-line)
        (delete-region (point) (line-end-position))
        (newline)
        (open-line 1))))
  (b/keymap-set message-mode-map "M-RET" #'b/newlines-or-asterism)

  (add-hook 'message-mode-hook #'flyspell-mode)
  (add-hook
   'message-mode-hook (lambda () (b/keymap-local-unset "C-c C-s"))))

;; (with-eval-after-load 'sendmail
;;   (setopt mail-header-separator ""))

;; (with-eval-after-load 'smtpmail
;;   (setopt smtpmail-queue-mail t
;;           smtpmail-queue-dir (concat b/maildir "queue/")))


;;; IRC
(with-eval-after-load 'erc
  (setopt
   erc-auto-query 'bury
   erc-autojoin-domain-only nil
   erc-dcc-get-default-directory (b/emacs.d "erc-dcc")
   erc-email-userid "bandali"
   ;; erc-join-buffer 'bury
   ;; 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)

  (if (version< erc-version "5.6-git")
      (setopt erc-format-nick-function #'erc-format-@nick)
    (setopt erc-show-speaker-membership-status t))

  (unless (version< erc-version "5.5")
    (setopt erc-rename-buffers t))

  (unless (version< erc-version "5.4")
    (declare-function
     erc-message "erc-backend" (message-command line &optional force))
    (declare-function erc-default-target "erc")
    (declare-function erc-current-nick "erc")
    (defun erc-cmd-OPME ()
      "Ask chanserv to op me in the current channel."
      (erc-message "PRIVMSG"
                   (format "chanserv op %s %s"
                           (erc-default-target)
                           (erc-current-nick))
                   nil))
    (declare-function erc-cmd-DEOP "erc" (&rest people))
    (defun erc-cmd-DEOPME ()
      "Deop myself in the current channel."
      (erc-cmd-DEOP (format "%s" (erc-current-nick)))))

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

  (setopt
   ;; erc-enable-logging 'erc-log-all-but-server-buffers
   erc-log-file-coding-system 'utf-8
   erc-log-write-after-insert t
   erc-log-write-after-send t
   erc-save-buffer-on-part nil
   erc-save-queries-on-quit nil)

  (with-eval-after-load 'erc-match
    (setopt
     erc-pal-highlight-type 'nick
     erc-pals
     '("corwin" "^gopar" "^iank" "^rwp" "technomancy" "thomzane"))
    (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 ",")
    ;; for matterircd nick (username) completions
    ;; (advice-add
    ;;  #'pcomplete-erc-nicks
    ;;  :around
    ;;  (lambda (orig-fun &rest args)
    ;;    (let ((nicks (apply orig-fun args)))
    ;;      (if (string-match-p "matterircd" (symbol-name (erc-network)))
    ;;          (mapcar (lambda (nick) (concat "@" nick)) nicks)
    ;;        nicks))))
    )

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

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

  (b/keymap-global-set "C-c w e" #'erc-switch-to-buffer-other-window)
  (b/keymap-set erc-mode-map "M-a" #'erc-track-switch-buffer))
(b/keymap-global-set
 "C-c e l"
 (lambda ()
   (interactive)
   (erc-tls :server "irc.libera.chat" :port 6697
            :client-certificate t)))
(b/keymap-global-set
 "C-c e o"
 (lambda ()
   (interactive)
   (erc-tls :server "irc.oftc.net" :port 6697
            :client-certificate t)))
(b/keymap-global-set
 "C-c e t"
 (lambda ()
   (interactive)
   (erc-tls :server "na.tilde.chat" :port 6697
            :client-certificate t)))

;; (b/keymap-global-set
;;  "C-c e c"
;;  (lambda ()
;;    (interactive)
;;    (erc :server "localhost" :port 6667
;;         :id 'matterircd-canonical)))


;;; Editing

;; 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
  (show-paren-mode 1))

(with-eval-after-load 'simple
  (setopt
   ;; 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))

(run-with-idle-timer 0.2 nil #'require 'savehist)
(with-eval-after-load 'savehist
  ;; Save minibuffer history.
  (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))

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

(with-eval-after-load 'text-mode
  (add-hook 'text-mode-hook #'flyspell-mode)
  (b/keymap-set text-mode-map "M-RET" #'b/insert-asterism))

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


;;; Programming modes

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

;; (add-to-list 'load-path (b/lisp "alloy-mode"))
;; (autoload 'alloy-mode "alloy-mode" nil t)
;; (with-eval-after-load 'alloy-mode
;;   (setq 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)))
;;   (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)))

;; (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)
;;   (setq 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
  (setopt sgml-basic-offset 0))

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

(add-hook 'tex-mode-hook #'auto-fill-mode)
(add-hook 'tex-mode-hook #'flyspell-mode)

(autoload 'cmake-mode "cmake-mode" nil t)
(add-to-list 'auto-mode-alist '("CMakeLists\\.txt\\'" . cmake-mode))
(add-to-list 'auto-mode-alist '("\\.cmake\\'" . cmake-mode))
(with-eval-after-load 'cmake-mode
  (add-to-list 'load-path (b/emacs.d "lisp/cmake-font-lock"))
  (require 'cmake-font-lock))


;;; Emacs enhancements & auxiliary packages
(with-eval-after-load 'man
  (setopt Man-width 80))

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

(with-eval-after-load 'eww
  (setopt
   eww-download-directory
   (file-name-as-directory (getenv "XDG_DOWNLOAD_DIR"))))
(b/keymap-global-set "C-c e w" #'eww)

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

(with-eval-after-load 'window
  (setopt split-width-threshold 140))

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

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

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

  (add-hook
   'po-mode-hook (lambda () (run-with-timer 0.1 nil #'View-exit)))
  (b/keymap-set po-mode-map "M-q" #'b/po-wrap))

(autoload #'po-mode "po-mode"
  "Major mode for editing PO translation files" t)
(add-to-list 'auto-mode-alist '("\\.po\\'\\|\\.po\\." . po-mode))

(add-to-list 'load-path (b/emacs.d "lisp/ffs"))
(run-with-idle-timer 0.5 nil #'require 'ffs)
(with-eval-after-load 'ffs
  (setopt ffs-default-face-height 250)
  (global-set-key (kbd "C-c f s") #'ffs))
(add-hook 'ffs-start-hook
          (lambda ()
            (mapc
             (lambda (mode) (funcall mode 1)) ; enable
             '(ffs--no-mode-line-minor-mode
               ffs--no-cursor-minor-mode))
            (mapc
             (lambda (mode) (funcall mode -1)) ; disable
             '(show-paren-local-mode
               display-battery-mode
               display-fill-column-indicator-mode
               flyspell-mode
               tool-bar-mode
               menu-bar-mode
               scroll-bar-mode))
            (fringe-mode 0)))
(add-hook 'ffs-quit-hook
          (lambda ()
            (mapc
             (lambda (mode) (funcall mode -1)) ; disable
             '(ffs--no-mode-line-minor-mode
               ffs--no-cursor-minor-mode))
            (mapc
             (lambda (mode) (funcall mode 1)) ; enable
             '(show-paren-local-mode
               display-battery-mode
               display-fill-column-indicator-mode
               flyspell-mode
               tool-bar-mode
               menu-bar-mode
               scroll-bar-mode))
            (fringe-mode nil)))

(add-to-list 'load-path (b/emacs.d "lisp/debian-el"))
(run-with-idle-timer 0.5 nil #'require 'debian-el)
(with-eval-after-load 'debian-el
  (require 'apt-sources)
  (require 'apt-utils)
  (require 'debian-bug)
  (require 'deb-view)
  (require 'gnus-BTS)
  (require 'preseed))

(add-to-list 'load-path (b/emacs.d "lisp/dpkg-dev-el"))
(run-with-idle-timer 0.5 nil #'require 'dpkg-dev-el)
(with-eval-after-load 'dpkg-dev-el
  (require 'debian-changelog-mode)
  (require 'debian-bts-control)
  (require 'debian-changelog-mode)
  (require 'debian-control-mode)
  (require 'debian-copyright)
  (require 'readme-debian))

;;; init.el ends here