;;  -*- lexical-binding: t; -*-

;;; programming languages
;;;; Erlang
(use-package erlang
  :ensure t
  :custom ((inferior-erlang-machine-options '("shell"))
           (inferior-erlang-machine "rebar3")
           (inferior-erlang-shell-type nil)
           (erlang-indent-level 4))

  ;; :bind (:map erlang-mode-map (("C-c C-z" . jao-vterm-repl-pop-to-repl)))

  :init
  ;; (require 'jao-vterm-repl)
  ;; (add-to-list 'auto-mode-alist '("^rebar\\.config\\`" . erlang-mode))
  ;; (jao-vterm-repl-register "rebar.config" "rebar3 shell" "^[0-9]+> ")

  :config
  ;; (defun jao-erlang-current-module ()
  ;;   (when (save-excursion (goto-char (point-min))
  ;;                         (re-search-forward "^-module(\\([^)]+\\))" nil t))
  ;;     (match-string-no-properties 1)))

  ;; (defun jao-erlang-compile (arg)
  ;;   (interactive "P")
  ;;   (save-some-buffers)
  ;;   (when-let ((mname (jao-erlang-current-module)))
  ;;     (with-current-buffer (jao-vterm-repl)
  ;;       (vterm-send-string (format "c(%s).\n" mname))
  ;;       (sit-for 0)
  ;;       (setq compilation-last-buffer (current-buffer))
  ;;       (when arg (jao-vterm-repl-pop-to-repl)))))

  ;; (setq erlang-shell-function #'jao-vterm-repl
  ;;       erlang-shell-display-function #'jao-vterm-repl-pop-to-repl
  ;;       erlang-compile-function #'jao-erlang-compile)
  )
;;;; Idris
(use-package idris-mode
  :ensure t
  :custom ((idris-interpreter-path "idris2")
           (idris-pretty-printer-width 80)
           (idris-repl-history-file "~/.emacs.d/cache/idris-history.eld")
           (idris-stay-in-current-window-on-compiler-error t)))
(jao-define-attached-buffer "^\\*idris.*")

;;;; Racket
(use-package racket-mode
  :ensure t
  :init (setq racket-show-functions '(racket-show-echo-area)
              racket-documentation-search-location 'local)
  :config
  (jao-define-attached-buffer "\\`\\*Racket REPL")
  (jao-define-attached-buffer "\\`\\*Racket Describe" 0.5)
  (add-hook 'racket-mode-hook #'paredit-mode)
  (require 'racket-xp)
  (add-hook 'racket-mode-hook #'racket-xp-mode)
  :bind (:map racket-xp-mode-map (("C-c C-S-d" . racket-xp-documentation)
                                  ("C-c C-d" . racket-xp-describe))))

;;; smart scan
(use-package smartscan
  :ensure t
  :commands smartscan-mode
  :init (add-hook 'prog-mode-hook #'smartscan-mode)
  :diminish)

;;; easy escape
(use-package easy-escape
  :ensure t
  :config
  (set-face-attribute 'easy-escape-face nil :underline t)
  (set-face-attribute 'easy-escape-delimiter-face nil :underline t)
  :hook (emacs-lisp-mode . easy-escape-minor-mode)
  :diminish (easy-escape-minor-mode . "^"))

;;; vterm
(use-package vterm
  :ensure t
  :demand t
  :commands (vterm vterm-mode)
  :init (setq vterm-kill-buffer-on-exit t
              vterm-copy-exclude-prompt t
              jao-use-vterm t)
  :config

  (defun jao-vterm-send-C-c () (interactive) (vterm-send-key "c" nil nil t))

  (jao-define-attached-buffer "\\*vterm\\*" 0.5)

  :bind (:map vterm-mode-map ("C-c C-c" . jao-vterm-send-C-c)))

(defun jao-exec-in-vterm (cmd bname)
  (if (string-blank-p (or cmd ""))
      (vterm)
    (let ((vterm-shell cmd)
          (vterm-kill-buffer-on-exit t)
          (buff (generate-new-buffer bname)))
      (switch-to-buffer buff)
      (vterm-mode))))
;;; ace window
(use-package ace-window
  :ensure t
  :demand t
  :init (setq aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l)
              aw-char-position 'top-left
              aw-ignore-current nil
              aw-dispatch-when-more-than 2
              aw-leading-char-style 'path
              aw-display-mode-overlay t
              aw-scope 'frame)
  :config

  (defun jao-ace-consult-buffer-other-window (w)
    (interactive)
    (aw-switch-to-window w)
    (consult-buffer))

  (setf (alist-get ?b aw-dispatch-alist)
        '(jao-ace-consult-buffer-other-window "Consult buffer"))

  (setf (alist-get ?B aw-dispatch-alist)
        (alist-get ?u aw-dispatch-alist))


  :bind (("M-o" . ace-window)
         ("M-O" . ace-swap-window)
         ("C-x 4 t" . ace-swap-window)))

;;; switch window
(use-package switch-window
  :ensure t
  :custom ((switch-window-minibuffer-shortcut ?z)
           (switch-window-background t)
           (switch-window-shortcut-style 'qwerty)
           (switch-window-shortcut-appearance 'text)
           (switch-window-timeout 7)
           (switch-window-threshold 2))
  :config
  (defun jao-switch-window--then (prompt cmd)
    (let ((f `(lambda ()
                (let ((default-directory ,default-directory))
                  (call-interactively ',cmd)))))
      (switch-window--then prompt f f)))

  (defun jao-switch-window-then-dired ()
    (interactive)
    (jao-switch-window--then "Find directory" 'dired))

  (defun jao-switch-window-then-find-file ()
    (interactive)
    (jao-switch-window--then "Find file" 'find-file))

  (defun jao-switch-window-then-consult-buffer ()
    (interactive)
    (jao-switch-window--then "Switch to buffer" 'consult-buffer))

  :bind (("s-o" . switch-window)
         ("C-x 4 d" . jao-switch-window-then-dired)
         ("C-x 4 f" . jao-switch-window-then-find-file)
         ("C-x 4 b" . jao-switch-window-then-consult-buffer)))

;;; git helpers
(use-package dired-git-info
  :ensure t
  :bind (:map dired-mode-map (")" . dired-git-info-mode)))

(use-package gist :ensure t)

;;; json
(use-package json-mode :ensure t)
(use-package json-navigator :ensure t)

;;; deft
(use-package deft
  :ensure t
  :after jao-org-notes
  :commands deft
  :init (setq deft-extensions '("org" "md")
              deft-directory jao-org-notes-dir
              deft-use-filename-as-title nil
              deft-use-filter-string-for-name t
              deft-file-naming-rules '((noslash . "-")
                                       (nospace . "-")
                                       (case-fn . downcase))
              deft-org-mode-title-prefix t
              deft-recursive t
              deft-recursive-ignore-dir-regexp (regexp-opt '("." ".." "attic"))
              deft-strip-summary-regexp
              (concat "\\([\n\t]"
                      "\\|^#\\+\\(title\\|created\\|date\\|author\\):.*$"
                      "\\|^#\\+\\(file\\)?tags: *\\)"))
  :config (setq deft-strip-title-regexp
                (concat "\\(^#\\+title: *\\)\\|" deft-strip-title-regexp))
  :bind (("<f9>" . deft)))

;;; detached
(use-package detached
  :ensure t
  :init
  (detached-init)
  :config
  (transient-define-prefix jao-transient-detached ()
    ["Detached sessions"
     ("v" "view session output" detached-view-session)
     ("a" "attach to a session" detached-attach-session)
     ("=" "diff a session with another session" detached-diff-session)
     ("c" "open the session output in compilation mode" detached-compile-session)
     ("r" "rerun a session" detached-rerun-session)
     ("i" "insert the session's command at point" detached-insert-session-command)
     ("w" "copy the session's shell command" detached-copy-session-command)
     ("W" "copy the session's output" detached-copy-session)
     ("k" "kill an active session" detached-kill-session)])

  :bind (;; Replace `async-shell-command' with `detached-shell-command'
         ([remap async-shell-command] . detached-shell-command)
         ;; Replace `compile' with `detached-compile'
         ([remap compile] . detached-compile)
         ([remap recompile] . detached-compile-recompile)
         ;; Replace built in completion of sessions with `consult'
         ([remap detached-open-session] . detached-consult-session)
         ("s-d" . jao-transient-detached))
  :custom ((detached-show-output-on-attach t)
           (detached-terminal-data-command system-type)))

(defun jao-detached-exec (command)
  (if (fboundp 'detached-create-session)
      (detached-create-session command)
    (jao-shell-exec command)))

;;; time display
(setq display-time-world-list
      '(("Europe/Paris" "Barcelona")
        ("America/Los_Angeles" "Los Angeles")
        ("America/New_York" "New York")
        ("Europe/London" "London")
        ("Asia/Calcutta" "Bangalore")
        ("Asia/Tokyo" "Tokyo")))

(defun jao-time--pdt-hour ()
  (jao-time-at-zone "%H" "America/Los_Angeles"))

(defun jao-time--chicago-hour ()
  (jao-time-at-zone "%H" "America/Chicago"))

(defun jao-time-at-zone (format zone)
  (set-time-zone-rule zone)
  (prog1 (format-time-string format)
    (set-time-zone-rule nil)))

(defun jao-time-echo-la-time ()
  (interactive)
  (message (jao-time-at-zone "LA %H:%M" "America/Los_Angeles")))

(defun jao-time-echo-times ()
  (interactive)
  (let ((msg (format "%s (%s)"
                     (format-time-string "%a, %e %B - %H:%M")
                     (jao-time-at-zone "%H:%M" "America/Los_Angeles"))))
    (jao-notify msg "" (jao-data-file "clock-world-icon.png"))))

(defun jao-time-to-epoch (&optional s)
  "Transform a time string to an epoch integer in milliseconds."
  (interactive)
  (let ((s (or s (read-string "Time string: " (thing-at-point 'string)))))
    (message "%s = %s"
             s
             (round (* 1000 (time-to-seconds (parse-time-string s)))))))

(defun jao-epoch-to-time (&optional v)
  "Transform an epoch, given in milliseconds, to a time string."
  (interactive)
  (let ((v (or v (read-number "Milliseconds: " (thing-at-point 'number)))))
    (message "%s = %s" v
             (format-time-string "%Y-%m-%d %H:%M:%S"
                                 (seconds-to-time (/ v 1000.0))))))
;;; mu4e
(jao-load-path "mu4e")
(use-package mu4e
  :init
  (setq mu4e-attachment-dir (expand-file-name "~/var/download/attachments")
        mu4e-change-filenames-when-moving nil
        mu4e-completing-read-function 'completing-read
        mu4e-display-update-status-in-modeline nil
        mu4e-get-mail-command "true" ;; "run-mb.sh || [ $? -eq 1 ]"
        mu4e-headers-show-threads t
        mu4e-headers-sort-direction 'ascending
        mu4e-headers-visible-columns 100
        mu4e-headers-visible-lines 12
        mu4e-hide-index-messages t
        mu4e-index-cleanup t  ;; don't do a full cleanup check
        mu4e-index-lazy-check t ;; don't consider up-to-date dirs
        mu4e-maildir "~/var/mail"
        mu4e-split-view 'horizontal ;; 'vertical
        mu4e-update-interval 300
        mu4e-use-fancy-chars nil
        mu4e-user-mail-address-list jao-mails
        mu4e-view-show-addresses t
        mu4e-view-show-images t
        mu4e-maildir-shortcuts '((:maildir "/jao/inbox" :key ?j)
                                 (:maildir "/bigml/inbox" :key ?b))
        jao-mu4e-uninteresting-mail-query
        (concat
         "flag:unread AND NOT flag:trashed"
         " AND NOT (maildir:/bigml/inbox OR maildir:/bigml/bugs OR"
         " maildir:/bigml/support OR maildir:/jao/inbox)")
        jao-mu4e-interesting-mail-query
        (concat
         "flag:unread AND NOT flag:trashed"
         " AND (maildir:/bigml/inbox OR maildir:/bigml/bugs OR"
         " maildir:/bigml/support OR maildir:/jao/inbox)")
        mu4e-bookmarks
        `((:name "Inbox" :query ,jao-mu4e-interesting-mail-query :key ?i)
          (:name "Other messages"
                 :query ,jao-mu4e-uninteresting-mail-query
                 :key 117)
          (:name "Today's messages" :query "date:today..now"
                 :key 116)
          (:name "Last 7 days" :query "date:7d..now" :hide-unread t
                 :key 119)
          (:name "Messages with PDFs"
                 :query "mime:application/pdf OR mime:x-application/pdf"
                 :key 112)))

  :config
  (defun jao-mu4e--maildir (msg)
    (when msg
      (let ((md (mu4e-message-field msg :maildir)))
        (when (string-match "/\\([^/]+\\)/.*" md)
          (match-string 1 md)))))

  (defun jao-mu4e--refile-folder (name)
    (lambda (msg)
      (let ((md (jao-mu4e--maildir msg)))
        (if (string= md name)
            (concat "/jao/" name)
          (format "/%s/%s" md name)))))

  (setq mu4e-sent-folder (jao-mu4e--refile-folder "sent"))
  (setq mu4e-drafts-folder (jao-mu4e--refile-folder "drafts"))
  (setq mu4e-trash-folder (jao-mu4e--refile-folder "trash"))
  (setq mu4e-refile-folder (jao-mu4e--refile-folder "trove"))

  (setq mu4e-contexts nil)

  (setq mu4e-view-show-images t)
  (when (fboundp 'imagemagick-register-types)
    (imagemagick-register-types))

  (define-key mu4e-view-mode-map [remap mu4e-view-verify-msg-popup]
              'epa-mail-verify)

  ;; View html message in browser (type aV)
  (add-to-list 'mu4e-view-actions
               '("ViewInBrowser" . mu4e-action-view-in-browser) t))

;;; twtxt
(use-package twtxt
  :ensure t
  :init (setq twtxt-file (expand-file-name "~/doc/jao.io/twtxt")
              twtxt-following
              '(("yarn" "https://twtxt.net/user/news/twtxt.txt"))))
;;; corfu bits
(defun jao-corfu-enable-no-auto ()
  (setq-local corfu-auto nil)
  (corfu-mode 1))

(defmacro jao-corfu-no-auto (mode)
  (let ((mode-name (intern (format "%s-mode" mode)))
        (hook-name (intern (format "%s-mode-hook" mode))))
    `(with-eval-after-load ',mode
       (add-to-list 'corfu-excluded-modes ',mode-name)
       (add-hook ',hook-name #'jao-corfu-enable-no-auto))))

(jao-corfu-no-auto eshell)

;;; gnus bits

(jao-transient-major-mode gnus-group
  ["Search"
   ("zc" "consult search" consult-notmuch)
   ("zf" "consult folder search" jao-consult-notmuch-folder)
   ("g" "gnus search" gnus-group-read-ephemeral-search-group)])

(defun jao-gnus-restart-servers ()
  (interactive)
  (message "Restarting all servers...")
  (gnus-group-enter-server-mode)
  (gnus-server-close-all-servers)
  (gnus-server-open-all-servers)
  (gnus-server-exit)
  (message "Restarting all servers... done"))

;;;; startup and kill
;; close gnus when closing emacs, but ask when exiting
(setq gnus-interactive-exit t)

(defun jao-gnus-started-hook ()
  (add-hook 'before-kill-emacs-hook 'gnus-group-exit))

(add-hook 'gnus-started-hook 'jao-gnus-started-hook)

(defun jao-gnus-after-exiting-hook ()
  (remove-hook 'before-kill-emacs-hook 'gnus-group-exit))

(add-hook 'gnus-after-exiting-gnus-hook 'jao-gnus-after-exiting-hook)

;; define a wrapper around the save-buffers-kill-emacs
;; to run the new hook before:
(advice-add 'save-buffers-kill-emacs :before (lambda ()
                                               (run-hooks 'before-kill-emacs-hook)))

(defadvice save-buffers-kill-emacs
    (before my-save-buffers-kill-emacs activate)
  "Install hook when emacs exits before emacs asks to save this and that."
  (run-hooks 'before-kill-emacs-hook))

(advice-remove 'ad-Advice-save-buffers-kill-emacs 'save-buffers-kill-emacs)

;;;; delayed expiry
(defvar jao-gnus--expire-every 50)
(defvar jao-gnus--get-count (1+ jao-gnus--expire-every))

(defun jao-gnus-get-new-news (&optional arg)
  (interactive "p")
  (when (and jao-gnus--expire-every
             (> jao-gnus--get-count jao-gnus--expire-every))
    (when jao-gnus-use-pm-imap (gnus-group-catchup "nnimap:pm/spam" t))
    (gnus-group-expire-all-groups)
    (setq jao-gnus--get-count 0))
  (setq jao-gnus--get-count (1+ jao-gnus--get-count))
  (gnus-group-get-new-news (max (if (= 1 jao-gnus--get-count) 4 3)
                                (or arg 0))))

(define-key gnus-group-mode-map "g" 'jao-gnus-get-new-news)
(define-key gnus-group-mode-map "\C-x\C-s" #'gnus-group-save-newsrc)

(defun jao-gnus--first-group ()
  (when (derived-mode-p 'gnus-group-mode)
    (gnus-group-first-unread-group)))

(with-eval-after-load "jao-afio"
  (add-hook 'jao-afio-switch-hook #'jao-gnus--first-group))

;;;; remove HTML from From contents (arxiv with r2e)
(require 'shr)
(defvar jao-gnus--from-rx
  (concat "From: \\\"?\\(  " jao-gnus--news-rx "\\)"))

(defun jao-gnus-remove-anchors ()
  (save-excursion
    (goto-char (point-min))
    (cond ((re-search-forward jao-gnus--from-rx nil t)
           (replace-match "" nil nil nil 1))
          ((re-search-forward "[gq].+ updates on arXiv.org: " nil t)
           (replace-match "")
           (let ((begin (point)))
             (when (re-search-forward "^\\(To\\|Subject\\):" nil t)
               (beginning-of-line)
               (let ((shr-width 10000))
                 (shr-render-region begin (1- (point))))))))))

(add-hook 'gnus-part-display-hook 'jao-gnus-remove-anchors)

;;;; find message id
(defun jao-gnus-file-message-id (filename)
  (with-temp-buffer
    (insert-file filename)
    (goto-char (point-min))
    (when (re-search-forward "^[Mm]essage-[Ii][Dd]: <?\\([^><]+\\)>?" nil t)
      (match-string 1))))
;;; old volume controls
(defun jao-player-volume-delta (raise)
  (jao-player-vol-delta (if raise 5 -5))
  (sit-for 0.05)
  (jao-player-show-volume))

(defun jao-player-volume-raise ()
  (interactive)
  (jao-player-volume-delta t))

(defun jao-player-volume-lower ()
  (interactive)
  (jao-player-volume-delta nil))

(defun jao-player-show-volume ()
  (interactive)
  (jao-notify "Volume" (format "%s%%" (jao-player-volume))))

;;; corfu
(use-package corfu
  :ensure t
  :init (setq corfu-echo-documentation 0.25
              corfu-cycle t
              corfu-count 15
              corfu-quit-no-match t
              corfu-auto t
              corfu-commit-predicate nil
              corfu-preview-current nil
              corfu-preselect-first t
              corfu-min-width 20
              corfu-max-width 100)
  :config

  ;; show eldoc string immediately after accepted completion too
  (with-eval-after-load "eldoc"
    (eldoc-add-command-completions "corfu-"))

  (defun jao-corfu-no-auto () (setq-local corfu-auto nil) (corfu-mode))

  (add-hook 'eshell-mode-hook #'jao-corfu-no-auto)

  (defun jao-corfu--active-p ()
    (and (>= corfu--index 0) (/= corfu--index corfu--preselect)))

  (defun jao-corfu-quit-or-insert ()
    (interactive)
    (if (jao-corfu--active-p) (corfu-insert) (corfu-quit)))

  (defun jao-corfu-quit-or-previous ()
    (interactive)
    (if (jao-corfu--active-p)
        (corfu-previous)
      (corfu-quit)
      (previous-line)))

  :bind (:map corfu-map
              ("C-<return>" . corfu-insert)
              ("\r" . jao-corfu-quit-or-insert)
              ("C-p" . jao-corfu-quit-or-previous)))

(defun corfu-in-minibuffer ()
  (when (not (bound-and-true-p vertico--input))
    (setq-local corfu-echo-documentation nil)
    (corfu-mode 1)))

(defun jao-corfu-maybe-enable ()
  (when (and (not jao-wayland-enabled) (display-graphic-p))
    (add-hook 'minibuffer-setup-hook #'corfu-in-minibuffer 1)
    (global-corfu-mode 1)))

(add-hook 'after-init-hook #'jao-corfu-maybe-enable)

;;; company
(use-package company
  :ensure t
  :custom ((company-backends '(company-capf
                               company-bbdb
                               company-files
                               company-dabbrev
                               company-keywords))
           (company-global-modes '(not slack-message-buffer-mode
                                       circe-channel-mode
                                       telega-chat-mode))
           (company-format-margin-function nil) ;; #'company-text-icons-margin
           (company-idle-delay 0.2)
           (company-lighter "")
           (company-lighter-base "")
           (company-show-numbers nil)
           (company-selection-wrap-around t)
           (company-tooltip-limit 15)
           (company-tooltip-align-annotations t)
           (company-tooltip-offset-display 'lines)) ;; 'scrollbar

  :config
  (defun jao-complete-at-point ()
    "Complete using company unless we're in the minibuffer."
    (interactive)
    (if (or (not company-mode) (window-minibuffer-p))
        (completion-at-point)
      (company-manual-begin)))

  (defun jao-company-use-in-tab ()
    (global-set-key [remap completion-at-point] #'jao-complete-at-point)
    (global-set-key [remap completion-symbol] #'jao-complete-at-point)
    (global-set-key (kbd "M-TAB") #'jao-complete-at-point))

  (jao-company-use-in-tab)

  :bind (:map company-active-map

              ("<tab>" . company-complete-common-or-cycle)
              ("TAB" . company-complete-common-or-cycle)

              ("C-h" . company-show-doc-buffer)
              ("M-." . company-show-location)
              ("C-<return>" . company-complete-selection)
              ([remap return] . company-abort)
              ("RET" . company-abort)

              :filter (or (not (derived-mode-p 'eshell-mode))
                          (company-explicit-action-p))
              ("<return>" . company-complete-selection)
              ("RET" . company-complete-selection))
  :diminish)

(unless (display-graphic-p) (global-company-mode 1))


;;; eldoc for magit status/log buffers
(defun jao-magit-eldoc-for-commit (_callback)
  (when-let ((commit (magit-commit-at-point)))
    (with-temp-buffer
      (magit-git-insert "show"
                        "--format=format:%an <%ae>, %ar"
                        (format "--stat=%d" (window-width))
                        commit)
      (goto-char (point-min))
      (put-text-property (point-min) (line-end-position) 'face 'bold)
      (buffer-string))))

(defun jao-magit-eldoc-setup ()
  (add-hook 'eldoc-documentation-functions
            #'jao-magit-eldoc-for-commit nil t)
  (eldoc-mode 1))

(add-hook 'magit-log-mode-hook #'jao-magit-eldoc-setup)
(add-hook 'magit-status-mode-hook #'jao-magit-eldoc-setup)

(with-eval-after-load "eldoc"
  (eldoc-add-command 'magit-next-line)
  (eldoc-add-command 'magit-previous-line)
  (eldoc-add-command 'magit-section-forward)
  (eldoc-add-command 'magit-section-backward))

;;; outline mode for notmuch tree view

(defun jao-notmuch-tree--msg-prefix (msg)
  (insert (propertize (if (plist-get msg :first) "> " "  ") 'display " ")))

(defun jao-notmuch-tree--mode-setup ()
  (setq-local outline-regexp "^> \\|^En")
  (outline-minor-mode t))

(defun jao-notmuch-tree-hide-others (&optional and-show)
  (interactive)
  (outline-hide-body)
  (outline-show-entry)
  (when and-show (notmuch-tree-show-message nil)))

(defsubst jao-notmuch-tree--message-open ()
  (and (buffer-live-p notmuch-tree-message-buffer)
       (get-buffer-window notmuch-tree-message-buffer)))

(defsubst jao-notmuch--get-prop (prop &optional props)
  (or (and props (plist-get props prop))
      (notmuch-tree-get-prop prop)
      (notmuch-show-get-prop prop)))

(defun jao-notmuch--looking-at-match-p ()
  (and (jao-notmuch--get-prop :match)
       (equal (jao-notmuch--get-prop :orig-tags)
              (jao-notmuch--get-prop :tags))))

(defun jao-notmuch-tree--next (prev thread no-exit &optional ignore-new)
  (let ((line-move-ignore-invisible nil))
    (cond ((and (not ignore-new)
                (jao-notmuch--looking-at-match-p)
                (not (jao-notmuch-tree--message-open))))
          (thread
           (notmuch-tree-next-thread prev)
           (unless (or (not (notmuch-tree-get-message-properties))
                       (jao-notmuch--looking-at-match-p))
             (notmuch-tree-matching-message prev (not no-exit))))
          (t (notmuch-tree-matching-message prev (not no-exit)))))
  (when (notmuch-tree-get-message-id)
    (jao-notmuch-tree-hide-others t))
  (when prev (forward-char 2)))

(defvar jao-notmuch-tree--prefix-map
  (let ((m (make-keymap "Thread operations")))
    (define-key m (kbd "TAB") #'outline-cycle)
    (define-key m (kbd "t") #'outline-toggle-children)
    (define-key m (kbd "s") #'outline-show-entry)
    (define-key m (kbd "S") #'outline-show-all)
    (define-key m (kbd "h") #'outline-hide-entry)
    (define-key m (kbd "H") #'outline-hide-body)
    (define-key m (kbd "o") #'jao-notmuch-tree-hide-others)
    (define-key m (kbd "n") #'outline-hide-other)
    m))

(defun jao-notmuch-tree-outline-setup (&optional prefix)
  (define-key notmuch-tree-mode-map (kbd (or prefix "T"))
              jao-notmuch-tree--prefix-map)
  (define-key notmuch-tree-mode-map (kbd "TAB") #'outline-cycle)
  (define-key notmuch-tree-mode-map (kbd "M-TAB") #'outline-cycle-buffer)
  (add-hook 'notmuch-tree-mode-hook #'jao-notmuch-tree--mode-setup)
  (advice-add 'notmuch-tree-insert-msg :before #'jao-notmuch-tree--msg-prefix))

(defun jao-notmuch-tree-next (thread &optional no-exit)
  "Next message or thread in forest, taking care of thread visibility."
  (interactive "P")
  (jao-notmuch-tree--next nil thread no-exit))

(defun jao-notmuch-tree-next-thread (&optional exit)
  "Next thread in forest, taking care of thread visibility."
  (interactive "P")
  (jao-notmuch-tree--next nil t exit))

(defun jao-notmuch-tree-previous (thread)
  "Previous message or thread in forest, taking care of thread visibility."
  (interactive "P")
  (jao-notmuch-tree--next t thread t))

(defun jao-notmuch-tree-previous-thread (&optional exit)
  "Previous thread in forest, taking care of thread visibility."
  (interactive "P")
  (jao-notmuch-tree--next t t exit))


;;; elpher/gemini
(use-package elpher :ensure t)
(defun jao-elpher--browse (url &rest _) (elpher-go url))
(add-to-list 'browse-url-handlers
             '("^\\(gemini\\|gopher\\)://.*" . jao-elpher--browse))

;;; fontsets
(defun jao--set-fontsets (_f)
  (when (and (display-graphic-p) (fboundp 'set-fontset-font))
    (set-fontset-font t 64257 "Quivira" nil)
    (set-fontset-font t 'egyptian "Noto Sans Egyptian Hieroglyphs" nil)
    (set-fontset-font t 'hangul "NanumGothicCoding" nil)
    (set-fontset-font t 'unicode (face-attribute 'default :family) nil)
    (set-fontset-font t 'unicode-bmp (face-attribute 'default :family) nil)
    (set-fontset-font t 'symbol "Symbola-10" nil)
    (set-fontset-font t 'greek "GFS Didot" nil)
    (set-fontset-font t 'mathematical "FreeSerif" nil)
    (set-fontset-font t 'emoji "Noto Color Emoji" nil)
    ;; boxes
    (set-fontset-font t '(9472 . 9599) "Source Code Pro" nil)
    ;; variation selector-16
    (set-fontset-font t 65039 "BabelStone Modern-1" nil)))

(jao--set-fontsets nil)
(add-to-list 'after-make-frame-functions 'jao--set-fontsets)

;;; eshell history completion to allow !$
;; This is done by advising eshell-history-reference to expand !$
;; into !!:$ which works...
(defadvice jao-eshell-history-reference (before ben-fix-eshell-history)
  "Fixes eshell history to allow !$ as abbreviation for !!:$"
  (when (string= (ad-get-arg 0) "!$") (ad-set-arg 0 "!!:$")))
(ad-activate 'jao-eshell-history-reference)
(add-hook 'eshell-expand-input-functions #'eshell-expand-history-references)
;;; enwc
(use-package enwc
  :ensure t
  :custom ((enwc-default-backend 'nm)
           (enwc-wired-device "wlp164s0")
           (enwc-wireless-device "wlp164s0")
           (enwc-display-mode-line nil)))


;;; tidal/mpc
(defconst jao-mpc--search-cmd
  "-f '%%album%% - %%artist%% :::%%file%%' search %s '%s'|grep :::tidal:album")

(defun jao-mpc--search-albums (query)
  (let* ((cmd (format jao-mpc--search-cmd "any" query))
         (str (jao-mpc--cmd cmd))
         (res (split-string str "\n" t)))
    (message "%s" str)
    (mapcar (lambda (s) (split-string s ":::" t " ")) res)))

(defun jao-mpc-select-tidal-album (&optional query port)
  (interactive "sSearch terms: ")
  (let* ((jao-mpc-port (or port jao-mpc-port))
         (resa (jao-mpc--search-albums query)))
    (if (null resa)
        (user-error "No results")
      (when-let* ((a (completing-read "Play album: " resa nil t))
                  (s (car (alist-get a resa nil nil 'string=))))
        (jao-mpc--add-and-play s port t)))))
;;; dogears
(use-package dogears
  :ensure t
  :enabled nil
  :bind (:map global-map
              ("M-g d" . dogears-go)
              ("M-g M-b" . dogears-back)
              ("M-g M-f" . dogears-forward)
              ("M-g M-d" . dogears-list)
              ("M-g M-D" . dogears-sidebar)))

(dogears-mode)
;;; pulsar
(use-package pulsar
  :ensure t
  :demand t
  :diminish
  :custom ((pulsar-pulse t)
           (pulsar-delay 0.1)
           (pulsar-iterations 10)
           (pulsar-face 'pulsar-yellow)
           (pulsar-highlight-face 'jao-themes--hilite))
  :config
  (dolist (f '(jao-prev-window
               jao-tracking-next-buffer
               smartscan-symbol-go-forward
               smartscan-symbol-go-backward))
    (add-to-list 'pulsar-pulse-functions f))

  :hook ((jao-afio-switch . pulsar-pulse-line)
         (consult-after-jump . pulsar-reveal-entry)
         (imenu-after-jump . pulsar-reveal-entry)
         (next-error . pulsar-pulse-line)))

(pulsar-global-mode 1)
;;;; mouse
(use-package disable-mouse
  :ensure t
  :diminish ((disable-mouse-global-mode . "")))

(global-disable-mouse-mode)
;;; tmr
(use-package tmr
  :ensure t
  :init
  (setq tmr-sound-file "/usr/share/sounds/freedesktop/stereo/message.oga"))
;;; pdf-tools
(use-package pdf-tools
  :ensure t
  :demand t
  :init
  (add-hook 'after-init-hook
            (lambda ()
              (setq pdf-view-midnight-colors
                    (cons (frame-parameter nil 'foreground-color)
                          (frame-parameter nil 'background-color)))))

  :hook ((pdf-view-mode . jao-doc-session-mark))

  :config (pdf-tools-install)

  :diminish ((pdf-view-midnight-minor-mode . ""))

  :bind (:map pdf-view-mode-map
              (("C-c C-d" . pdf-view-midnight-minor-mode)
               ("j" . pdf-view-next-line-or-next-page)
               ("J" . pdf-view-scroll-up-or-next-page)
               ("k" . pdf-view-previous-line-or-previous-page)
               ("K" . pdf-view-scroll-down-or-previous-page))))
;;; slack
(eval-and-compile
  (defvar jao-slack-dir (expand-file-name "emacs-slack" jao-local-lisp-dir)))

(use-package slack
  :commands (slack-start)
  :vc t
  :load-path jao-slack-dir
  :init
  (setq slack-alert-icon (jao-data-file "slack.svg")
        slack-buffer-emojify nil
        slack-buffer-create-on-notify t
        slack-display-team-name t
        slack-typing-visibility 'buffer ;; 'never, 'buffer, 'frame
        slack-thread-also-send-to-room t
        slack-profile-image-file-directory "/tmp/slack-imgs/"
        slack-image-file-directory "/tmp/slack-imgs/"
        slack-file-dir "~/var/download/slack/"
        slack-prefer-current-team t
        slack-message-tracking-faces '(warning)
        slack-log-level 'warn
        slack-message-custom-notifier (lambda (_msg room _team) room))
  :bind (:map slack-mode-map (("@" . slack-message-embed-mention)
                              ("#" . slack-message-embed-channel))
              :map slack-message-buffer-mode-map
              (("C-c C-e" . slack-message-edit)
               ("C-c C-a" . slack-file-upload)))
  :hook ((slack-file-info-buffer-mode . view-mode))
  :config

  (defun my-slack-nobreak-mrkdwn ()
    "Return non-nil (don't break line) if point is in markdown code face."
    (seq-find (lambda (ov)
                (eq 'slack-mrkdwn-code-block-face (overlay-get ov 'face)))
              (overlays-at (point))))
  (add-hook 'slack-message-buffer-mode-hook
            (lambda ()
              (add-hook 'fill-nobreak-predicate #'my-slack-nobreak-mrkdwn
                        nil 'local)))

  (dolist (f (list slack-file-dir slack-image-file-directory))
    (when (not (file-exists-p f)) (make-directory f)))

  (jao-shorten-modes 'slack-message-buffer-mode
                     'slack-thread-message-buffer-mode)
  (jao-tracking-faces 'warning)
  (jao-tracking-cleaner "logstash-\\([^-]+\\)-\\(.+\\)"  "\\2-\\1")
  (jao-tracking-cleaner
   "^\\*Slack - .*? : \\(MPIM: \\)?\\([^ ]+\\)\\( \\(T\\)\\)?.*" "\\2\\4")
  (jao-define-attached-buffer "\\*Slack .+ Edit Message [0-9].+" 20))

;;; alert
(use-package alert
  :ensure t
  :init
  (setq alert-default-style 'message ;; 'libnotify
        alert-hide-all-notifications nil))
;;; snippets
(defun jao-org-notes-open-tags ()
  "Search for a note file, matching all tags with completion."
  (let* ((tags (jao-org-notes--read-tags))
         (fn (lambda ()
               (prog1 (jao-org-notes--find-tag (car tags))
                 (setq tags (cdr tags)))))
         (res (funcall fn)))
    (while (and res tags) (setq res (seq-intersection res (funcall fn))))
    (unless res (user-error "No notes found"))
    (when-let (f (completing-read "Select file: " (mapcar #'car res)))
      (find-file (cadr (assoc f res))))))

(defun jao-sway-run-or-focus-tidal ()
  (interactive)
  (if (jao-shell-running-p "tidal-hifi")
      (jao-swaymsg "[app_id=tidal-hifi] scratchpad show")
    (let ((c
           "tidal-hifi --enable-features=UseOzonePlatform --ozone-platform=wayland &"))
      (start-process-shell-command "tidal-hifi" nil c))
    (jao-sway-run-or-focus-tidal)))

;;

(defun jao-afio--set-mode-line ()
  (when (and window-system (fboundp 'jao-mode-line-hide-inactive))
    (if (string= "docs" (jao-afio-frame-name))
        (jao-mode-line-show-inactive nil)
      (jao-mode-line-hide-inactive nil))))

(unless jao-modeline-in-minibuffer
  (add-hook 'jao-afio-switch-hook #'jao-afio--set-mode-line))

;;

(defun jao-word-definition-lookup ()
  "Look up the word under cursor in a browser."
  (interactive)
  (require 'thingatpt)
  (browse-url
   (concat "http://www.wordnik.com/words/"
           ;; "http://www.answers.com/main/ntquery?s="
           (thing-at-point 'word))))

;;

(defun jao-notmuch-format-author (width msg)
  (let* ((headers (plist-get msg :headers))
         (auth (notmuch-tree-clean-address (plist-get headers :From)))
         (awidth (string-width auth))
         (auth (if (> awidth width)
                   (substring auth 0 width)
                 (concat auth (make-string (- width awidth) 32))))
         (face (if (plist-get msg :match)
                   'notmuch-tree-match-author-face
                 'notmuch-tree-no-match-author-face)))
    (propertize auth 'face face)))