config.phundrak.com/docs/emacs/packages/applications.org

65 KiB
Raw Permalink Blame History

Emacs — Packages — Applications

Applications

(mapconcat (lambda (line)
             (let* ((key      (nth 0 line))
                    (function (nth 1 line))
                    (comment  (or (nth 2 line) ""))
                    (package  (or (nth 3 line) "")))
               (format "\"%s%s\" %s"
                       prefix
                       key
                       (if (string= "" comment)
                           (if (member function '("" "nil")) "nil" (concat "#'" function))
                         (format "'(%s :wk %s%s)"
                                 (if (member function '("" "nil")) ":ignore t" function)
                                 (if (member function '("none" "nil")) "t" (concat "\"" comment "\""))
                                 (if (string-blank-p package) "" (concat ":package " package)))))))
           table
           "\n")
(use-package transient
  :straight (:build t)
  :defer t)

Bitwarden

This package is still a very much work in progress one Im developing in order to interact with Bitwarden in Emacs with the help of the Bitwarden CLI. Use at your own risks.

(use-package bitwarden
  :defer t
  :straight (bitwarden :build t
                       :type git
                       :host nil
                       :repo "https://labs.phundrak.com/phundrak/bitwarden.el"))

Calendar

I am using the built-in calendar of Emacs, but as I use Evil and the bépo layout, many keybindings available by default or through evil-collection dont fit my needs perfectly.

(use-package calendar
  :straight (:type built-in)
  :defer t
  :general
  (:keymaps 'calendar-mode-map
   "»"     #'calendar-scroll-left
   "«"     #'calendar-scroll-right
   "M-»"   #'calendar-scroll-left-three-months
   "M-«"   #'calendar-scroll-right-three-months
   "M-r"   #'calendar-scroll-left
   "M-c"   #'calendar-scroll-right
   "M-S-r" #'calendar-scroll-left-three-months
   "M-S-c" #'calendar-scroll-right-three-months
   "q"     #'calendar-exit)
  (phundrak/major-leader-key
    :keymaps 'calendar-mode-map
    "="   #'calendar-count-days-region
    "^"   '(:ignore t :wk "beginning")
    "^ w" #'calendar-beginning-of-week
    "^ m" #'calendar-beginning-of-month
    "^ y" #'calendar-beginning-of-year
    "$"   '(:ignore t :wk "end")
    "$w"  #'calendar-end-of-week
    "$m"  #'calendar-end-of-month
    "$y"  #'calendar-end-of-year
    "a"   '(:ignore t :wk "appointment")
    "aa"  #'appt-add
    "ad"  #'appt-delete
    "A"   #'org-calendar-goto-agenda
    "b"   '(:ignore t :wk "go back")
    "bd"  '(calendar-backward-day :wk "a day")
    "bw"  '(calendar-backward-week :wk "a week")
    "bm"  '(calendar-backward-month :wk "a month")
    "by"  '(calendar-backward-year :wk "a year")
    "d"   '(:ignore t :wk "diary")
    "da"  '(diary-show-all-entries :wk "all entries")
    "dd"  '(diary-view-entries :wk "entries")
    "dm"  #'diary-mark-entries
    "do"  #'diary-view-other-diary-entries
    "di"  '(:ignore t :wk "insert")
    "die" '(diary-insert-entry :wk "entry")
    "die" '(diary-insert-block-entry :wk "block entry")
    "diw" '(diary-insert-weekly-entry :wk "weekly")
    "dim" '(diary-insert-monthly-entry :wk "monthly")
    "diy" '(diary-insert-yearly-entry :wk "yearly")
    "F"   '(:ignore t :wk "files")
    "Fa"  #'org-calendar-goto-agenda
    "f"   '(:ignore t :wk "go forward")
    "fd"  '(calendar-forward-day :wk "a day")
    "fw"  '(calendar-forward-week :wk "a week")
    "fm"  '(calendar-forward-month :wk "a month")
    "fy"  '(calendar-forward-year :wk "a year")
    "g"   '(:ignore t :wk "goto")
    "gd"  #'calendar-goto-date
    "gm"  #'calendar-other-month
    "gt"  #'calendar-goto-today
    "h"   '(:ignore t :wk "holidays")
    "hl"  #'calendar-list-holidays
    "hm"  #'calendar-mark-holidays
    "H"   '(:ignore t :wk "HTML export")
    "Hm"  #'cal-html-cursor-month
    "Hy"  #'cal-html-cursor-year
    "M"   #'calendar-other-month))

Docker

Docker is an awesome tool for reproducible development environments. Due to this, I absolutely need a mode for editing Dockerfiles.

(use-package dockerfile-mode
  :defer t
  :straight (:build t)
  :hook (dockerfile-mode . lsp-deferred)
  :init
  (put 'docker-image-name 'safe-local-variable #'stringp)
  :mode "Dockerfile\\'"
  :general
  (phundrak/major-leader-key
   :keymaps 'general-mode-map
   :packages 'lsp-mode
   "l"  '(:keymap lsp-command-map :which-key "lsp")))

The docker package also provides interactivity with Docker and docker-compose from Emacs.

(use-package docker
  :defer t
  :straight (:build t))

Elfeed

Elfeed is a nice Atom and RSS reader for Emacs. The only thing I want to change for now is the default search filter: I want to see not only unread news but read news as well, a bit like my emails; and where the database is to be stored.

(use-package elfeed
  :defer t
  :straight (:build t)
  :config
  <<elfeed-open-youtube-with-mpv>>
  :custom
  ((elfeed-search-filter "@6-months-ago")
   (elfeed-db-directory  (expand-file-name ".elfeed-db"
                                           user-emacs-directory))))

I dont want YouTube videos to be open with my web browser when I invoke elfeed-show-visit, so Ill advise this function to make it possible to modify the behaviour of said function. Oh, and I already made a neat package for playing YouTube videos and friends through youtube-dl or its superior fork yt-dlp in mpv.

(defun my/elfeed-filter-youtube-videos (orig-fun &rest args)
  "Open with mpv the video leading to PATH"
  (let ((link (elfeed-entry-link elfeed-show-entry)))
    (when link
      (if (string-match-p ".*youtube\.com.*watch.*" link)
          ;; This is a YouTube video, open it with mpv
          (progn
            (require 'ytplay)
            (ytplay link))
        (apply orig-fun args)))))

(advice-add 'elfeed-show-visit :around #'my/elfeed-filter-youtube-videos)

A future improvement to be made is to let the user chose the resolution of the video before it is launched. I may not always have the best internet connection, and viewing 4K videos on a 1080p display is not something very useful.

Elfeed-goodies is a package which enhances the Elfeed experience. Aside from running its setup command as soon as possible, I also set in this code block all my keybinds for Elfeed here.

(use-package elfeed-goodies
  :defer t
  :after elfeed
  :commands elfeed-goodies/setup
  :straight (:build t)
  :init
  (elfeed-goodies/setup)
  :custom
  (elfeed-goodies/feed-source-column-width 28)
  (elfeed-goodies/tag-column-width 28)
  :general
  (phundrak/undefine
    :keymaps '(elfeed-show-mode-map elfeed-search-mode-map)
    :packages 'elfeed
    "DEL" nil
    "s"   nil)
  (phundrak/evil
    :keymaps 'elfeed-show-mode-map
    :packages 'elfeed
    <<general-keybindings-gen(table=elfeed-keybinds-show-mode)>>)
  (phundrak/evil
    :keymaps 'elfeed-search-mode-map
    :packages 'elfeed
    <<general-keybindings-gen(table=elfeed-keybinds-search-mode)>>)
  (phundrak/major-leader-key
    :keymaps 'elfeed-search-mode-map
    :packages 'elfeed
    <<general-keybindings-gen(table=elfeed-keybinds-search-mode-prefixed)>>))

Last but not least, my Elfeed configuration is stored in an org file thanks to elfeed-org.

(use-package elfeed-org
  :defer nil
  :after elfeed
  :straight (:build t)
  :init
  (elfeed-org)
  :config
  (setq rmh-elfeed-org-files '("~/org/elfeed.org")))

Keybinds

First, here are the keybinds for Elfeeds elfeed-show-mode. They arent prefixed by SPC like most of my keybinds, a direct keypress will directly launch the function.

Key Function Comment
+ elfeed-show-tag
- elfeed-show-untag
« elfeed-show-prev
» elfeed-show-next
b elfeed-show-visit
C elfeed-kill-link-url-at-point
d elfeed-show-save-enclosure
l elfeed-show-next-link
o elfeed-goodies/show-ace-link
q elfeed-kill-buffer
S elfeed-show-new-live-search
u elfeed-show-tagunread
y elfeed-show-yank

Same thing, different mode, here are my keybinds for elfeed-search-mode.

Key Function Comment
« elfeed-search-first-entry
» elfeed-search-last-entry
b elfeed-search-browse-url
f filter
fc elfeed-search-clear-filter
fl elfeed-search-live-filter
fs elfeed-search-set-filter
u update
us elfeed-search-fetch
uS elfeed-search-update
uu elfeed-update
uU elfeed-search-updateforce
y elfeed-search-yank

I have some additional keybinds for elfeed-search-mode, but these one are prefixed with , (and M-m).

Key Function Comment
c elfeed-db-compact
t tag
tt elfeed-search-tag-all-unread
tu elfeed-search-untag-all-unread
tT elfeed-search-tag-all
tU elfeed-search-untag-all

Email

Basic configuration

As seen below, I use org-msg to compose my emails, which includes by default my signature. Therefore, there is no need for Emacs itself to know about it since I dont want it to include it a second time after org-msg already did.

(setq message-signature nil
      mail-signature    nil)

Gnus

(use-package gnus
  :straight (:type built-in)
  :defer t
  :config
  (require 'gnus-topic)
  (setq gnus-select-method '(nnnil))
  (setq gnus-secondary-select-methods '((nntp "news.gwene.org")))

  (setq gnus-asynchronous t ;; async
        gnus-use-article-prefetch 15
        ;; article
        gnus-visible-headers (mapcar (lambda (str) (concat "^" str ":"))
                                     '("From" "To" "Cc" "Subject" "Newsgroup"
                                       "Date" "Followup-To" "Reply-To"
                                       "Organization" "X-Newsreader" "X-Mailer"))
        gnus-sorted-header-list gnus-visible-headers
        gnus-thread-sort-functions '(gnus-thread-sort-by-number
                                     gnus-thread-sort-by-subject
                                     (not gnus-thread-sort-by-date))
        ;; group
        gnus-level-subscribed 6
        gnus-level-unsubscribed 7
        gnus-level-zombie 8
        gnus-group-sort-function '((gnus-group-sort-by-unread)
                                   (gnus-group-sort-by-alphabet)
                                   (gnus-group-sort-by-rank))
        gnus-group-line-format "%M%p%P%5y:%B%(%g%)\n"
        gnus-group-mode-line-format "%%b"
        gnus-topic-display-empty-topics nil
        ;; summary
        gnus-auto-select-first nil
        gnus-summary-ignore-duplicates t
        gnus-suppress-duplicates t
        gnus-summary-to-prefix "To:"
        gnus-summary-line-format "%U%R %-18,18&user-date; %4L:%-25,25f %B%s\n"
        gnus-summary-mode-line-format "[%U] %p"
        gnus-sum-thread-tree-false-root ""
        gnus-sum-thread-tree-indent " "
        gnus-sum-thread-tree-single-indent ""
        gnus-sum-thread-tree-leaf-with-other "+->"
        gnus-sum-thread-tree-root ""
        gnus-sum-thread-tree-single-leaf "\\->"
        gnus-sum-thread-tree-vertical "|")

  (add-hook 'dired-mode-hook #'gnus-dired-mode)
  (add-hook 'gnus-group-mode-hook #'gnus-topic-mode)
  (add-hook 'gnus-select-group-hook #'gnus-group-set-timestamp)

  (dolist (mode '(gnus-group-mode-hook gnus-summary-mode-hook gnus-browse-mode-hook))
    (add-hook mode #'hl-line-mode))

  :general
  (phundrak/evil
    :keymaps 'gnus-summary-mode-map
    :packages 'gnus
    "«" #'gnus-summary-prev-article
    "»" #'gnus-summary-next-article)
  (phundrak/major-leader-key
    :keymaps 'gnus-summary-mode-map
    :packages 'gnus
    "d"  #'gnus-summary-delete-article
    "f"  #'gnus-summary-mail-forward
    "r"  '(:ignore t :wk "reply")
    "rr" #'gnus-summary-reply-with-original
    "rl" #'gnus-summary-reply-to-list-with-original
    "rw" #'gnus-summary-wide-reply-with-original
    "rW" #'gnus-summary-very-wide-reply-with-original)
  (phundrak/evil
    :keymaps 'gnus-group-mode-map
    :packages 'gnus
    "«" #'gnus-group-prev-group
    "»" #'gnus-group-next-group)
  (phundrak/major-leader-key
    :keymaps '(gnus-group-mode-map)
    :packages 'gnus
    "SPC" #'gnus-topic-read-group
    "c"  '(gnus-topic-catchup-articles :which-key "catchup")
    "f"  '(gnus-fetch-group :which-key "fetch")
    "j"  '(:ignore t :which-key "jump")
    "jg" #'gnus-group-jump-to-group
    "jt" #'gnus-topic-jump-to-topic
    "L"  #'gnus-group-list-all-groups
    "n"  #'gnus-group-news
    "t"  '(gnus-group-topic-map :which-key "topics")
    "u"  #'gnus-group-unsubscribe))

Mu4e

Mu4e is a very eye-pleasing email client for Emacs, built around mu and which works well with mbsync (found in Archs isync package). For me, the main advantage of mu4e is it has a modern interface for emailing, and quite straightforward. I tried a couple of other email clients for Emacs, and I even was for some time a Gnus user, but in the end, mu4e really works best for me. Below youll find my configuration for the mu4e package itself.

(use-package mu4e
  :after all-the-icons
  :straight (:build t)
  :commands mu4e mu4e-compose-new
  :init
  (defun mu4e--main-action-str (name func)
    "This seems to be needed until evil-collection supports the latest
  version of mu4e."
    "mu4e-main-action")

  (require 'mu4e)
  (remove-hook 'mu4e-main-mode-hook 'evil-collection-mu4e-update-main-view)

  <<mu4e-ical-setup>>
  <<mu4e-ical-init-config>>

  (setopt mu4e-completing-read-function 'completing-read
          mu4e-use-fancy-chars          t
          message-kill-buffer-on-exit   t
          mu4e-org-support              nil)
  (let ((dir (concat (getenv "HOME") "/Downloads/mu4e")))
    (when (file-directory-p dir)
      (setopt mu4e-attachment-dir dir)))
  (defmacro mu4e-view-mode--prepare ()
    `(lambda () (visual-line-mode 1)))
  :gfhook ('mu4e-view-mode-hook (mu4e-view-mode--prepare))
  :general
  (phundrak/evil
    :keymaps 'mu4e-main-mode-map
    :packages 'mu4e
    "U" #'mu4e-update-index)
  :config
  (require 'mu4e-view)
  (with-eval-after-load 'mm-decode
    (add-to-list 'mm-discouraged-alternatives "text/html")
    (add-to-list 'mm-discouraged-alternatives "text-richtext"))

  (add-hook 'mu4e-view-mode-hook (lambda () (setq truncate-lines nil)))
  (add-hook 'mu4e-headers-mode-hook (lambda () (setq truncate-lines t)))

  <<mu4e-ical-config>>
  <<mu4e-keybindings-undef>>
  <<mu4e-keybindings-view>>
  <<mu4e-keybindings-view-no-prefix>>
  <<mu4e-keybindings-header>>
  <<mu4e-keybindings-header-no-leader>>
  <<mu4e-keybindings-message>>

  <<mu4e-mail-service>>
  <<mu4e-mail-on-machine>>
  <<mu4e-no-signature>>
  <<mu4e-bookmarks>>
  <<mu4e-maildirs>>

  (when (fboundp 'imagemagick-register-types)
    (imagemagick-register-types))

  (require 'gnus-dired)
  (setq gnus-dired-mail-mode 'mu4e-user-agent)

  (add-hook 'mu4e-compose-mode-hook (lambda () (use-hard-newlines t 'guess)))
  (add-hook 'mu4e-compose-mode-hook 'mml-secure-message-sign-pgpmime)

  (setq mu4e-change-filenames-when-moving t
        mu4e-update-interval              60
        mu4e-compose-format-flowed        t
        mu4e-view-show-addresses          t
        mu4e-sent-messages-behaviour      'sent
        mu4e-hide-index-messages          t
        mu4e-view-show-images             t                     ; try to show images
        mu4e-view-image-max-width         600
        message-send-mail-function        #'smtpmail-send-it    ; how to send an email
        smtpmail-stream-type              'starttls
        message-kill-buffer-on-exit       t                     ; close after sending
        mu4e-context-policy               'pick-first           ; start with first (default) context
        mu4e-compose-context-policy       'ask-if-none          ; compose with current context, or ask
        mu4e-completing-read-function     #'ivy-completing-read ; use ivy
        mu4e-confirm-quit                 t                     ; no need to ask
        mu4e-header-fields                '((:account    . 12)
                                            (:human-date . 12)
                                            (:flags      . 4)
                                            (:from       . 25)
                                            (:subject)))

  ;; set mail user agent
  (setq mail-user-agent 'mu4e-user-agent)

  <<mu4e-fancy-marks>>
  <<mu4e-vertical-split>>
  <<mu4e-headers-mode>>

  (defun mu4e-action-open-as-pdf (msg)
    "Export and open MSG as pdf."
    (let* ((date    (mu4e-message-field msg :date))
           (infile  (mu4e~write-body-to-html msg))
           (outfile (format-time-string "/tmp/%Y-%m-%d-%H-%M-%S.pdf" date)))
      (with-temp-buffer
        (shell-command
         (format "wkhtmltopdf %s %s" infile outfile) t))
      (find-file outfile))))

Quick sidenote: on Arch Linux, youll need to install either mu or mu-git from the AUR in order to use mu4e. I also have a .desktop file to make it possible to open mu4e directly from my program picker. It uses the shell script emacsmail Ive written here.

[Desktop Entry]
Name=Mu4e
GenericName=Mu4e
Comment=Maildir Utils for Emacs
MimeType=x-scheme-handler/mailto;
Exec=/home/phundrak/.local/bin/emacsmail %U
Icon=emacs
Type=Application
Terminal=false
Categories=Network;Email;TextEditor
StartupWMClass=Gnus
Keywords=Text;Editor;
Interact with iCalendar events

Something that has always bugged me was how to properly with iCalendar events shared with me on my personal mailbox. The answer is actually dead simple.

(require 'mu4e-icalendar)
(mu4e-icalendar-setup)

Ill just configure a couple of things regarding these events, namely converting them to org-mode and add them to my ~/org/notes.org file, and delete the email once I answered to the event.

(setq mu4e-icalendar-trash-after-reply t)
(setq gnus-icalendar-org-capture-file "~/org/notes.org"
      gnus-icalendar-org-capture-headline '("Calendar"))
(gnus-icalendar-org-setup)
Basic configuration

First, lets inform Emacs how it can send emails, using which service and how. In my case, I use my own mail server.

(setq smtpmail-smtp-server       "mail.phundrak.com"
      smtpmail-smtp-service      587
      smtpmail-stream-type       'starttls
      message-send-mail-function 'smtpmail-send-it)

We also need to inform it on where my emails are stored on my machine, and how to retrieve them.

(setq mu4e-get-mail-command "mbsync -a"
      mu4e-root-maildir     "~/Mail"
      mu4e-trash-folder     "/Trash"
      mu4e-refile-folder    "/Archive"
      mu4e-sent-folder      "/Sent"
      mu4e-drafts-folder    "/Drafts")

In the same vein of this bit of configuration, I do not want mu4e to insert my mail signature, org-msg already does that.

(setq mu4e-compose-signature nil)
Bookmarks

In mu4e, the main focus isnt really mail directories such as your inbox, your sent messages and such, but instead you manipulate bookmarks which will show you emails depending on tags. This mean you can create some pretty customized bookmarks that go way beyound your simple inbox, outbox and all. Actually, four of my bookmarks have a couple of filtering:

  • anything in my inbox linked to my university
  • the emacs-doctor mailing list (French Emacs mailing list)
  • emails related to my internship
  • and my inbox for any mail not caught by any of these filters

And all of them will have the requirement not to display any trashed email. Actually, all of my bookmarks will have this requirement, except for the bookmark dedicated to them as well as my sent emails. Ill add these latter requirements later.

Here are the requirements for my university bookmark. The regex matches any email address which contains either up8.edu or univ-paris8, which can be found in email addresses from the University Paris 8 (my university).

(mapconcat (lambda (list)
             (let ((address (string-replace (regexp-quote "~")
                                            ""
                                            (if (stringp list) list (car list)))))
               (mapconcat (lambda (flag)
                            (concat flag ":" address))
                          '("list" "t" "f" "contact")
                          " OR ")))
           lists
           " OR ")
(let ((regex "/.*up8\\.edu|.*univ-paris8.*/"))
  (concat
   <<mu4e-bookmarks-from-copy-to-gen>>
   " OR maildir:/Univ/Inbox OR maildir:/Univ/Junk"))
f:/.*up8\.edu|.*univ-paris8.*/ OR c:/.*up8\.edu|.*univ-paris8.*/ OR t:/.*up8\.edu|.*univ-paris8.*/ OR maildir:/Univ/Inbox OR maildir:/Univ/Junk

Next I need an inbox dedicated to the association Im part of.

(let ((regex "/.*supran\\.fr/"))
  <<mu4e-bookmarks-from-copy-to-gen>>)
f:/.*supran\.fr/ OR c:/.*supran\.fr/ OR t:/.*supran\.fr/

As for the Emacs-doctor list, I need to match both the current, modern mailing list address but also its old address. The same applies for the emacs-devel mailing list as well as GitHub emails related to my package eshell-info-banner.el. Here are the addresses to match:

  • /ateliers.*emacs.*/
  • /emacs-.*@gnu.org/
  • /.*eshell-info-banner.*/
  • /.*emacsfr.*/
"<<mu4e-bookmarks-mailing-lists(lists=mu4e-emacs-mailing-lists)>>"
list:/ateliers.*emacs.*/ OR t:/ateliers.*emacs.*/ OR f:/ateliers.*emacs.*/ OR contact:/ateliers.*emacs.*/ OR list:/emacs-.*@gnu.org/ OR t:/emacs-.*@gnu.org/ OR f:/emacs-.*@gnu.org/ OR contact:/emacs-.*@gnu.org/ OR list:/.*eshell-info-banner.*/ OR t:/.*eshell-info-banner.*/ OR f:/.*eshell-info-banner.*/ OR contact:/.*eshell-info-banner.*/ OR list:/.*emacsfr.*/ OR t:/.*emacsfr.*/ OR f:/.*emacsfr.*/ OR contact:/.*emacsfr.*/

Another bookmark I wish to have is one dedicated to emails related to more general development topics, including issues and PRs from GitHub.

  • /.*\\.github\\.com/
  • /.*\\.gitlab\\.com/
  • stumpwm-devel@nongnu.org
  • /.*sr\\.ht/
;; "<<mu4e-bookmarks-mailing-lists(lists=mu4e-github-mailing-lists)>> AND NOT ()"
(string-join '("<<mu4e-bookmarks-mailing-lists(lists=mu4e-github-mailing-lists)>>"
               "AND NOT ("
               <<mu4e-bookmarks-filter-emacs-list>>
               ")")
             " ")
list:/.*\.github\.com/ OR t:/.*\.github\.com/ OR f:/.*\.github\.com/ OR contact:/.*\.github\.com/ OR list:/.*\.gitlab\.com/ OR t:/.*\.gitlab\.com/ OR f:/.*\.gitlab\.com/ OR contact:/.*\.gitlab\.com/ OR list:stumpwm-devel@nongnu.org OR t:stumpwm-devel@nongnu.org OR f:stumpwm-devel@nongnu.org OR contact:stumpwm-devel@nongnu.org OR list:/.*sr\.ht/ OR t:/.*sr\.ht/ OR f:/.*sr\.ht/ OR contact:/.*sr\.ht/ AND NOT ( list:/ateliers.*emacs.*/ OR t:/ateliers.*emacs.*/ OR f:/ateliers.*emacs.*/ OR contact:/ateliers.*emacs.*/ OR list:/emacs-.*@gnu.org/ OR t:/emacs-.*@gnu.org/ OR f:/emacs-.*@gnu.org/ OR contact:/emacs-.*@gnu.org/ OR list:/.*eshell-info-banner.*/ OR t:/.*eshell-info-banner.*/ OR f:/.*eshell-info-banner.*/ OR contact:/.*eshell-info-banner.*/ OR list:/.*emacsfr.*/ OR t:/.*emacsfr.*/ OR f:/.*emacsfr.*/ OR contact:/.*emacsfr.*/ )

When it comes to my internship, all emails will contain an address containing aubay.com (thats where my internship takes place).

  • /.*aubay.com/
(let ((regex "/.*aubay\\.com/"))
  <<mu4e-bookmarks-from-copy-to-gen>>)
f:/.*aubay\.com/ OR c:/.*aubay\.com/ OR t:/.*aubay\.com/

As I said earlier, something that will often come back in my bookmarks is the emails must not be trashed to appear. I want also to display junk emails, so I end up with the following rule:

(mapconcat #'identity
           `("NOT flag:trashed"
             ,(format "(%s)" (mapconcat (lambda (maildir) (concat "maildir:" maildir))
                                        '("/Inbox" "/Junk")
                                        " OR ")))
            " AND ")
NOT flag:trashed AND (maildir:/Inbox OR maildir:/Junk)

And for the last string-generating code, lets describe my main inbox:

(mapconcat #'identity
           (cons "<<mu4e-bookmarks-default-filter()>>"
                 `(,(format "(%s)"
                            <<mu4e-bookmarks-filter-aubay>>)
                   ,(format "(%s)" "<<mu4e-bookmarks-filter-asso()>>")
                   ,(format "(%s)"
                            <<mu4e-bookmarks-filter-emacs-list>>)
                   ,(format "(%s)"
                            <<mu4e-bookmarks-filter-github-list>>)
                   ,(format "(%s)"
                            <<mu4e-bookmarks-filter-uni>>)))
           " AND NOT ")
NOT flag:trashed AND (maildir:/Inbox OR maildir:/Junk) AND NOT (f:/.*aubay\.com/ OR c:/.*aubay\.com/ OR t:/.*aubay\.com/) AND NOT (f:/.*supran.fr/ OR c:/.*supran.fr/ OR t:/.*supran.fr/) AND NOT (list:/ateliers.*emacs.*/ OR t:/ateliers.*emacs.*/ OR f:/ateliers.*emacs.*/ OR contact:/ateliers.*emacs.*/ OR list:/emacs-.*@gnu.org/ OR t:/emacs-.*@gnu.org/ OR f:/emacs-.*@gnu.org/ OR contact:/emacs-.*@gnu.org/ OR list:/.*eshell-info-banner.*/ OR t:/.*eshell-info-banner.*/ OR f:/.*eshell-info-banner.*/ OR contact:/.*eshell-info-banner.*/ OR list:/.*emacsfr.*/ OR t:/.*emacsfr.*/ OR f:/.*emacsfr.*/ OR contact:/.*emacsfr.*/) AND NOT (list:/.*\.github\.com/ OR t:/.*\.github\.com/ OR f:/.*\.github\.com/ OR contact:/.*\.github\.com/ OR list:/.*\.gitlab\.com/ OR t:/.*\.gitlab\.com/ OR f:/.*\.gitlab\.com/ OR contact:/.*\.gitlab\.com/ OR list:stumpwm-devel@nongnu.org OR t:stumpwm-devel@nongnu.org OR f:stumpwm-devel@nongnu.org OR contact:stumpwm-devel@nongnu.org OR list:/.*sr\.ht/ OR t:/.*sr\.ht/ OR f:/.*sr\.ht/ OR contact:/.*sr\.ht/ AND NOT ( list:/ateliers.*emacs.*/ OR t:/ateliers.*emacs.*/ OR f:/ateliers.*emacs.*/ OR contact:/ateliers.*emacs.*/ OR list:/emacs-.*@gnu.org/ OR t:/emacs-.*@gnu.org/ OR f:/emacs-.*@gnu.org/ OR contact:/emacs-.*@gnu.org/ OR list:/.*eshell-info-banner.*/ OR t:/.*eshell-info-banner.*/ OR f:/.*eshell-info-banner.*/ OR contact:/.*eshell-info-banner.*/ OR list:/.*emacsfr.*/ OR t:/.*emacsfr.*/ OR f:/.*emacsfr.*/ OR contact:/.*emacsfr.*/ )) AND NOT (f:/.*up8\.edu|.*univ-paris8.*/ OR c:/.*up8\.edu|.*univ-paris8.*/ OR t:/.*up8\.edu|.*univ-paris8.*/ OR maildir:/Univ/Inbox OR maildir:/Univ/Junk)

We can finally define our bookmarks! The code reads as follows:

(setq mu4e-bookmarks
      `((:name "Inbox"
         :key ?i
         :query ,(format "%s"
                         <<mu4e-bookmarks-inbox-filters>>))
        (:name "Internship"
         :key ?a
         :query ,(format "(%s) AND (%s)"
                     "<<mu4e-bookmarks-default-filter()>>"
                     <<mu4e-bookmarks-filter-aubay>>))
        (:name "University"
         :key ?u
         :query ,(format "%s AND %s"
                         "<<mu4e-bookmarks-default-filter()>>"
                         "<<mu4e-bookmarks-filter-uni()>>"))
        (:name "Dev"
         :key ?d
         :query ,(format "%s AND (%s)"
                         "<<mu4e-bookmarks-default-filter()>>"
                         "<<mu4e-bookmarks-filter-github-list()>>"))
        (:name "Emacs"
         :key ?e
         :query ,(format "%s AND %s"
                         "<<mu4e-bookmarks-default-filter()>>"
                         <<mu4e-bookmarks-filter-emacs-list>>))
        (:name "Supran"
         :key ?s
         :query ,(format "%s AND %s"
                         "<<mu4e-bookmarks-default-filter()>>"
                         "<<mu4e-bookmarks-filter-asso()>>"))
        (:name "Sent"       :key ?S :query "maildir:/Sent OR maildir:/Univ/Sent")
        (:name "All Unread" :key ?U :query "flag:unread AND NOT flag:trashed")
        (:name "Today"      :key ?t :query "date:today..now AND NOT flag:trashed")
        (:name "This Week"  :key ?w :query "date:7d..now    AND NOT flag:trashed")
        (:name "This Month" :key ?m :query "date:1m..now    AND NOT flag:trashed")
        (:name "This Year"  :key ?y :query "date:1y..now    AND NOT flag:trashed")))
Maildirs

Sometimes, bookmarks are a bit too restrictive when I want to search for stuff. Simply jumping to a mail directory, or maildir, is more than enough. In mu4e, with my current setup, I can jump to any maildir with the shortcut Jo in normal-mode, and I just need to choose in a list the maildir I want to jump to. But it can be faster.

With this piece of code, I can simply jump to my inbox maildir with Ji, to my sent messages with Js, and so on.

(setq mu4e-maildir-shortcuts
      '((:maildir "/Inbox" :key ?i)
        (:maildir "/Sent" :key ?s)
        (:maildir "/Junk" :key ?j)
        (:maildir "/Trash" :key ?t)))
Dealing with spammers

Im sure you have received at least one email recently from a sketchy email address asking you something that might be completely unrelated to what you do, or at least somewhat related. Fortunately, we have a hero! Now, let me write a function that will insert their pre-written text at point to avoid me going back to their Twitter thread each time I want to shut spammers up.

(defun reply-to-bill ()
  (interactive)
  (insert "Please forward this email to bill@noprocurement.com,
and delete my email, as Ill be changing jobs soon, and this
email address will no longer be active.

Bill Whiskoney is a senior partner at Nordic Procurement
Services, and he handles our budget and will help you further or
introduce you to someone who can."))

If you want the full story, make sure to read the whole thread, I guarantee it, its worth your time! And in case the Twitter thread disappear in the future, here is a backup.

Getting Fancy

Im not a huge fan of mu4es default icons marking my emails, so Ill redefine them as follows. Be aware the name of these icons are from faicon in the package all-the-icons.

Mark Flag Icon
draft D pencil
flagged F flag
new N rss
passed P check
replied R reply
seen S eye
unread u eye-slash
trashed T trash
attach a paperclip
encrypted x lock
signed s certificate
(mapconcat (lambda (line)
             (let ((mark (car line))
                   (flag (cadr line))
                   (icon (caddr line)))
               (format "mu4e-headers-%s-mark `(\"%s\" . ,(all-the-icons-faicon \"%s\" :height 0.8))"
                       mark
                       flag
                       icon)))
           table
           "\n")
mu4e-headers-draft-mark `("D" . ,(all-the-icons-faicon "pencil" :height 0.8))
mu4e-headers-flagged-mark `("F" . ,(all-the-icons-faicon "flag" :height 0.8))
mu4e-headers-new-mark `("N" . ,(all-the-icons-faicon "rss" :height 0.8))
mu4e-headers-passed-mark `("P" . ,(all-the-icons-faicon "check" :height 0.8))
mu4e-headers-replied-mark `("R" . ,(all-the-icons-faicon "reply" :height 0.8))
mu4e-headers-seen-mark `("S" . ,(all-the-icons-faicon "eye" :height 0.8))
mu4e-headers-unread-mark `("u" . ,(all-the-icons-faicon "eye-slash" :height 0.8))
mu4e-headers-trashed-mark `("T" . ,(all-the-icons-faicon "trash" :height 0.8))
mu4e-headers-attach-mark `("a" . ,(all-the-icons-faicon "paperclip" :height 0.8))
mu4e-headers-encrypted-mark `("x" . ,(all-the-icons-faicon "lock" :height 0.8))
mu4e-headers-signed-mark `("s" . ,(all-the-icons-faicon "certificate" :height 0.8))

Lets enable them and set them:

(setq mu4e-use-fancy-chars t
      <<mu4e-fancy-marks-gen()>>)
(defun my/set-mu4e-headers-width ()
  (let ((width    (window-body-width))
        (threshold (+ 120 80)))
    (setq mu4e-split-view (if (> width threshold)
                              'vertical
                            'horizontal))))

(setq mu4e-headers-visible-columns 120
      mu4e-headers-visible-lines   15)
(add-hook 'mu4e-headers-mode-hook #'my/set-mu4e-headers-width)
Headers mode
(add-hook 'mu4e-headers-mode-hook (lambda () (visual-line-mode -1)))
(add-hook 'mu4e-headers-mode-hook (lambda () (toggle-truncate-lines -1)))
Keybindings

By default, Evil has some pretty annoying keybindings for users of the bépo layout: hjkl becomes ctsr for us. Lets undefine some of these:

(phundrak/undefine
  :keymaps 'mu4e-view-mode-map
  :packages 'mu4e
  "S"   nil
  "r"   nil
  "c"   nil
  "gu"  nil)

(phundrak/undefine
  :keymaps '(mu4e-view-mode-map mu4e-headers-mode-map)
  :packages 'mu4e
  "s"   nil)

Now, lets define some keybindings for mu4es view mode, that is when we are viewing an email. All these keybindings will reside between the major-mode specific leader key , and most of these keybindings can be described with a simple function:

Keybinding Function Description
& mu4e-view-pipe
. mu4e-headers-split-adjust-width/body mu4e-headers width
a nil attachments
a& mu4e-view-pipe-attachment
aa mu4e-view-attachment-action
ao mu4e-view-open-attachment
aO mu4e-view-open-attachment-with
c nil compose
cc mu4e-compose-new
ce mu4e-compose-edit
cf mu4e-compose-forward
cr mu4e-compose-reply
cR mu4e-compose-resend
l mu4e-show-log
m nil mark
md mu4e-view-mark-for-trash
mD mu4e-view-mark-for-delete
mm mu4e-view-mark-for-move
mr mu4e-view-mark-for-refile
mR mu4e-view-mark-for-read
mu mu4e-view-mark-for-unread
mU mu4e-view-mark-for-unmark
t mu4e-view-mark-thread mark thread
T nil toggle
Tc mu4e-view-toggle-hide-cited
Th mu4e-view-toggle-html
n mu4e-view-headers-next
N mu4e-view-headers-next-unread
p mu4e-view-headers-prev
P mu4e-view-headers-prev-unread
u nil url
uf mu4e-view-go-to-url
uF mu4e-view-fetch-url
(phundrak/major-leader-key
  :keymaps 'mu4e-view-mode-map
  :packages 'mu4e
  <<general-keybindings-gen(table=mu4e-keybindings-view-tbl)>>)

Two other keybinds are added without a prefix, just for the sake of convenience.

(phundrak/evil
  :keymaps 'mu4e-view-mode-map
  :packages 'mu4e
  "«" #'mu4e-view-headers-prev
  "»" #'mu4e-view-headers-next)

Ill also declare two keybinds for mu4es headers mode.

(phundrak/major-leader-key
  :keymaps 'mu4e-headers-mode-map
  :packages 'mu4e
  "t" '(mu4e-view-mark-thread :which-key "mark thread")
  "s" 'swiper)

I will also redefine without a leader key ctsr in order to be able to move freely (remember, bépo layout for me).

Key Function Comment
c evil-backward-char
t evil-next-visual-line
s evil-previous-visual-line
r evil-forward-char
(phundrak/evil
  :keymaps 'mu4e-headers-mode-map
  :packages 'mu4e
  <<general-keybindings-gen(table=mu4e-keybindings-header-no-leader-table)>>)

Finally, lets declare a couple of keybindings for when we are composing a message. This time, all my keybindings are prefixed with the major-mode leader and call a simple function.

Key Function Description
, message-send-and-exit
c message-send-and-exit
a message-kill-buffer
k message-kill-buffer
s message-dont-send
f mml-attach-file
(phundrak/major-leader-key
  :keymaps 'message-mode-map
  :packages 'mu4e
  <<general-keybindings-gen(table=mu4e-keybindings-message-tbl)>>)

Composing messages

Org mime is cool and all, you can write some org-mode and then export it to either a plain-text or an HTML email. BUT, have you considered skipping the export part and write your emails directly in org-mode?

(use-package org-msg
  :after (mu4e)
  :straight (:build t)
  :hook ((mu4e-compose-pre . org-msg-mode))
  :custom-face
  (mu4e-replied-face ((t (:weight normal :foreground "#b48ead"))))
  :config
  (defun my/org-msg-signature-convert (orig-fun &rest args)
    "Tweak my signature when replying as plain/text only."
    (let ((res (apply orig-fun args)))
      (when (equal (cadr args) '(text))
        (setf (alist-get 'signature res)
              (replace-regexp-in-string "\n+" "\n" org-msg-signature)))
      res))
  (advice-add 'org-msg-composition-parameters
              :around 'my/org-msg-signature-convert)
  (add-hook 'mu4e-headers-mode (lambda () (toggle-truncate-lines -1)))
  (setq org-msg-startup              "inlineimages"
        org-msg-default-alternatives '((new           . (text))
                                       (reply-to-html . (text))
                                       (reply-to-text . (text)))
        org-msg-greeting-name-limit  3
        org-msg-signature            (format "\n--\n#+begin_signature\n%s\n#+end_signature"
                                             (with-temp-buffer
                                               (insert-file-contents mail-signature-file)
                                               (while (re-search-forward "\n" nil t)
                                                 (replace-match "\n\n"))
                                               (buffer-string))))
  :general
  (phundrak/major-leader-key
    :keymaps 'org-msg-edit-mode-map
    :packages 'org-msg
    <<general-keybindings-gen(table=org-msg-edit-mode-keybinds)>>))

The keybinds are relatively simple org-msg-edit-mode:

Key Function Description
, message-send-and-exit
c message-send-and-exit
a message-kill-buffer
k message-kill-buffer
s message-dont-send
f org-msg-attach

Email alerts

There is also a package for mu4e which generates desktop notifications when new emails are received. By default, I want to be notified by all messages in my inbox and junk folder. Also, Ill use Emacs default notification system, and Ill activate the modeline notification.

(use-package mu4e-alert
  :straight (:build t)
  :after mu4e
  :defer t
  :init
  (add-hook 'after-init-hook #'mu4e-alert-enable-notifications)
  (add-hook 'after-init-hook #'mu4e-alert-enable-mode-line-display)
  (mu4e-alert-set-default-style 'notifications)
  :config
  (setq mu4e-alert-interesting-mail-query "flag:unread"))

EMMS and Media

EMMS, also known as the Emacs MultiMedia System, allows the user to interact through Emacs with multimedia elements such as music and videos. My main use for it will be for music with MPD (see its configuration here).

(use-package emms
  :defer t
  :after all-the-icons
  :straight (:build t)
  :init
  (require 'emms-setup)
  (require 'emms-mark)
  (emms-all)
  (add-to-list 'emms-info-functions 'emms-info-mpd)
  (add-to-list 'emms-player-list    'emms-player-mpd)
  (emms-player-mpd-sync-from-mpd)
  (emms-player-mpd-connect)

  <<emms-fd-name>>
  <<emms-fd-function>>
  <<emms-search-set-variable>>

  <<emms-media-hydra>>
  (defun my/mpc-toggle ()
    (interactive)
    (shell-command-and-echo "mpc toggle"))
  (defun emms-player-toggle-pause ()
    (interactive)
    (shell-command-and-echo "mpc toggle"))
  :custom
  ((emms-source-file-default-directory (expand-file-name "~/Music"))
   (emms-player-mpd-server-name "localhost")
   (emms-player-mpd-server-port "6600")
   (emms-player-mpd-music-directory (expand-file-name "~/Music"))
   (emms-browser-thumbnail-small-size 64)
   (emms-browser-thumbnail-medium-size 128)
   (emms-browser-covers #'emms-browser-cache-thumbnail-async)
   (emms-playlist-default-major-mode 'emms-mark-mode))
  :general
  (phundrak/undefine
    :keymaps 'emms-browser-mode-map
    :packages 'emms
    "s"   nil
    "r"   nil)
  (phundrak/evil
    :keymaps 'emms-browser-mode-map
    :packages 'emms
    "a"  #'emms-browser-add-tracks
    "A"  #'emms-browser-add-tracks-and-play
    "b"  '(:ignore t :which-key "browse by")
    "bA" #'emms-browse-by-album
    "ba" #'emms-browse-by-artist
    "bg" #'emms-browse-by-genre
    "bs" #'emms-smart-browse
    "by" #'emms-browse-by-year
    "c"  #'emms-browser-clear-playlist
    "S"  '(:ignore t :which-key "search")
    "SA" '(emms-browser-search-by-album  :which-key "by album")
    "Sa" '(emms-browser-search-by-artist :which-key "by artist")
    "Ss" '(emms-browser-search-by-names  :which-key "by name")
    "St" '(emms-browser-search-by-names  :which-key "by title")
    "q"  #'kill-this-buffer)
  (phundrak/evil
    :keymaps 'emms-playlist-mode-map
    :packages 'emms
    "d" #'emms-playlist-mode-kill-track
    "p" #'emms-playlist-mode-play-smart
    "q" #'kill-this-buffer))

Finding files from EMMS

EMMS has two default ways of finding files: either a built-in function relatively slow but portable, or with find which is arguably faster but less portable. Honestly, I dont care about portability, Ill always use this Emacs config on Linux, but I dont want to use find either since there is something even faster: fd.

First well declare a variable that can hold the path to the fd executable:

(defvar emms-source-file-fd (executable-find "fd"))

Then, we need to declare a new function that will use fd to find files. The function, as specified by the documentation of emms-source-file-directory-tree-function, receives two arguments dir and regex. We can work with that!

(defun emms-source-file-directory-tree-fd (dir regex)
  "Return a list of all files under DIR that match REGEX.
This function uses the external fd utility. The name for fd may
be supplied using `emms-source-file-fd'."
  (with-temp-buffer
    (call-process emms-source-file-fd
                  nil t nil
                  "-L" ; follow symlinks
                  regex
                  "-t f"
                  (expand-file-name dir))
    (delete ""
            (split-string (buffer-substring (point-min)
                                          (point-max))
                          "\n"))))

We can finally set this function as our search function.

(setq emms-source-file-directory-tree-function #'emms-source-file-directory-tree-fd)

Keybinds

I also want to create a small hydra for manipulating MPD:

(defun shell-command-and-echo (command &optional echo prefix)
  "Run COMMAND and display the result of ECHO prefixed by PREFIX.

Run COMMAND as a shell command.

If ECHO is non nil, display the result of its execution as a
shell command to the minibuffer through `MESSAGE'.

If PREFIX is non nil, it will prefix the output of ECHO in the
minibuffer, both separated by a single space."
  (progn
    (with-temp-buffer
      (shell-command command
                     (current-buffer)
                     (current-buffer))
      (when echo
        (message "%s%s"
                 (if prefix
                     (concat prefix " ")
                   "")
                 (string-trim
                  (shell-command-to-string "mpc volume")))))))

(defhydra hydra-media (:hint nil)
  "
     %s(all-the-icons-material \"volume_up\" :height 1.0 :v-adjust -0.2)
     [_s_]
« [_c_] _p_ [_r_] » [_S_] %s(all-the-icons-material \"stop\")
     [_t_]
     %s(all-the-icons-material \"volume_down\" :height 1.0 :v-adjust -0.2)
"
  ("c" emms-player-mpd-previous)
  ("r" emms-player-mpd-next)
  ("t" (shell-command-and-echo "mpc volume -2" "mpc volume" "mpc"))
  ("s" (shell-command-and-echo "mpc volume +2" "mpc volume" "mpc"))
  ("p" (shell-command-and-echo "mpc toggle"))
  ("S" emms-player-mpd-stop)
  ("q" nil :exit t))

Nov

Nov is a major-mode for reading EPUB files within Emacs. Since I have it, I dont need any other Epub reader on my computer! Plus this one is customizable and programmable, why would I use any other EPUB reader?

(use-package nov
  :straight (:build t)
  :defer t
  :mode ("\\.epub\\'" . nov-mode)
  :general
  (phundrak/evil
    :keymaps 'nov-mode-map
    :packages 'nov
    "c"   #'nov-previous-document
    "t"   #'nov-scroll-up
    "C-d" #'nov-scroll-up
    "s"   #'nov-scroll-down
    "C-u" #'nov-scroll-down
    "r"   #'nov-next-document
    "gm"  #'nov-display-metadata
    "gn"  #'nov-next-document
    "gp"  #'nov-previous-document
    "gr"  #'nov-render-document
    "gt"  #'nov-goto-toc
    "gv"  #'nov-view-source
    "gV"  #'nov-view-content-source)
  :config
  (setq nov-text-width 95))

PDF Tools

pdf-tools enables PDF support for Emacs, much better than its built-in support with DocView. Aside from the classical settings such as keybinds, I also enable the midnight colours by default; think of it as an equivalent of Zathuras recolour feature which kind of enables a dark mode for PDFs.

(use-package pdf-tools
  :defer t
  :magic ("%PDF" . pdf-view-mode)
  :straight (:build t)
  :hook (pdf-tools-enabled . pdf-view-midnight-minor-mode)
  :general
  (phundrak/evil
    :keymaps 'pdf-view-mode-map
    :packages 'pdf-tools
    "y"   #'pdf-view-kill-ring-save
    "t"   #'evil-collection-pdf-view-next-line-or-next-page
    "s"   #'evil-collection-pdf-view-previous-line-or-previous-page)
  (phundrak/major-leader-key
    :keymaps 'pdf-view-mode-map
    :packages 'pdf-tools
    "a"  '(:ignore t :which-key "annotations")
    "aD" #'pdf-annot-delete
    "at" #'pdf-annot-attachment-dired
    "ah" #'pdf-annot-add-highlight-markup-annotation
    "al" #'pdf-annot-list-annotations
    "am" #'pdf-annot-markup-annotation
    "ao" #'pdf-annot-add-strikeout-markup-annotation
    "as" #'pdf-annot-add-squiggly-markup-annotation
    "at" #'pdf-annot-add-text-annotation
    "au" #'pdf-annot-add-underline-markup-annotation

    "f"  '(:ignore t :which-key "fit")
    "fw" #'pdf-view-fit-width-to-window
    "fh" #'pdf-view-fit-height-to-window
    "fp" #'pdf-view-fit-page-to-window

    "s"  '(:ignore t :which-key "slice/search")
    "sb" #'pdf-view-set-slice-from-bounding-box
    "sm" #'pdf-view-set-slice-using-mouse
    "sr" #'pdf-view-reset-slice
    "ss" #'pdf-occur

    "o"  'pdf-outline
    "m"  'pdf-view-midnight-minor-mode)
  :config
  (with-eval-after-load 'pdf-view
    (setopt pdf-view-midnight-colors '("#d8dee9" . "#2e3440"))))

One thing pdf-tools doesnt handle is restoring the PDF to the last point it was visited — in other words, open the PDF where I last left it.

(use-package pdf-view-restore
  :after pdf-tools
  :defer t
  :straight (:build t)
  :hook (pdf-view-mode . pdf-view-restore-mode)
  :config
  (setq pdf-view-restore-filename (expand-file-name ".tmp/pdf-view-restore"
                                                    user-emacs-directory)))

Project Management

Magit

Magit is an awesome wrapper around Git for Emacs! Very often, I go from disliking to really hating Git GUI clients because they often obfuscate which Git commands are used to make things happen. Such a thing doesnt happen with Magit, its pretty transparent, but it still provides some awesome features and visualizations of what you are doing and what Git is doing! In short, I absolutely love it!

(use-package magit
  :straight (:build t)
  :defer t
  :init
  (setq forge-add-default-bindings nil)
  :config
  (add-hook 'magit-process-find-password-functions 'magit-process-password-auth-source)
  <<magit-angular-keywords-highlight>>
  (setopt magit-clone-default-directory "~/fromGIT/"
          magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
  (with-eval-after-load 'evil-collection
    (phundrak/evil
      :packages '(evil-collection magit)
      :keymaps '(magit-mode-map magit-log-mode-map magit-status-mode-map)
      :states 'normal
      "t" #'magit-tag
      "s" #'magit-stage))
  :general
  (:keymaps '(git-rebase-mode-map)
   :packages 'magit
   "C-t" #'evil-next-line
   "C-s" #'evil-previous-line)
  (phundrak/major-leader-key
    :keymaps 'git-rebase-mode-map
    :packages 'magit
    "," #'with-editor-finish
    "k" #'with-editor-cancel
    "a" #'with-editor-cancel))

There is currently a bug in Emacs TRAMP as described in issue #4720 of Magit and bug 62093 of Emacs. A workaround is to redefine the old tramp-send-command function through an advice.

(defun my--tramp-send-command--workaround-stty-icanon-bug (conn-vec orig-command &rest args)
  "See: https://github.com/magit/magit/issues/4720"
  (let ((command
         (if (string= "stty -icrnl -icanon min 1 time 0" orig-command)
             "stty -icrnl"
           orig-command)))
    (append (list conn-vec command) args)))

(defun my--tramp-send-command--workaround-stty-icanon-bug--filter-args (args)
  (apply #'my--tramp-send-command--workaround-stty-icanon-bug args))

(advice-add 'tramp-send-command :filter-args
            #'my--tramp-send-command--workaround-stty-icanon-bug--filter-args)

I also want to highlight these angular-style keywords in commit messages.

(defun my/magit-log-highlight-angular-keywords (_rev msg)
  "Highlight angular-style keywords in commit messages."
  (let ((boundary 0))
    (when (string-match (rx (seq (or "feat" "fix" "docs" "style" "refactor"
                                     "perf" "test" "ci" "chore")
                                 (* "(" (* (not ")")) ")")
                                 ":"))
                        msg
                        boundary)
        (setq boundary (match-end 0))
        (magit--put-face (match-beginning 0) boundary
                         'magit-keyword msg)))
  msg)

(advice-add #'magit-log-propertize-keywords
            :after
            #'my/magit-log-highlight-angular-keywords)

Alphapapa also created an awesome package for Magit: magit-todos which display in the Magit buffer a list of TODOs found in the current project to remind you of what to do next.

First, lets setup our todo keywords with hl-todo. A good few todo keywords are already defined in the hl-todo-keyword-faces variable. Why not use them? hl-todo-mode enables fontlock highlight of these keywords in a buffer. Lets enable this mode globally.

(use-package hl-todo
  :defer t
  :straight (:build t)
  :init (global-hl-todo-mode 1))

We can now configure properly magit-todos. Notice my custom function hooked to magit-mode-hook. This is because this package tries to find TODOs in all files in the current project, and my yadm repositorys root is my $HOME. So, yeah, no magit-todos in yadm.

(use-package magit-todos
  :straight (:build t)
  :after (magit hl-todo)
  :init
  (with-eval-after-load 'magit
   (defun my/magit-todos-if-not-yadm ()
     "Deactivate magit-todos if in yadm Tramp connection.
If `magit--default-directory' points to a yadm Tramp directory,
deactivate `magit-todos-mode', otherwise enable it."
     (if (string-prefix-p "/yadm:" magit--default-directory)
         (magit-todos-mode -1)
       (magit-todos-mode +1)))
   (add-hook 'magit-mode-hook #'my/magit-todos-if-not-yadm))
  :config
  (setq magit-todos-ignore-case nil))

Forge

Forge acts as an interface for GitHub, GitLab, and Bitbucket inside Magit. A lot of possibilities are present, you can read issues and pull requests, create them, and fork projects among other things.

NOTE: Make sure to configure a GitHub token before using this package!

(use-package forge
  :after magit
  :straight (:build t)
  :init
  (evil-collection-forge-setup)
  :general
  (phundrak/major-leader-key
    :keymaps 'forge-topic-mode-map
    "c"  #'forge-create-post
    "e"  '(:ignore t :which-key "edit")
    "ea" #'forge-edit-topic-assignees
    "ed" #'forge-edit-topic-draft
    "ek" #'forge-delete-comment
    "el" #'forge-edit-topic-labels
    "em" #'forge-edit-topic-marks
    "eM" #'forge-merge
    "en" #'forge-edit-topic-note
    "ep" #'forge-edit-post
    "er" #'forge-edit-topic-review-requests
    "es" #'forge-edit-topic-state
    "et" #'forge-edit-topic-title))

Projectile

First, I need to install ripgrep, a faster reimplementation of grep, which will be very useful when managing projects.

(use-package ripgrep
  :if (executable-find "rg")
  :straight (:build t)
  :defer t)

Now, I can use projectile, which is sort of the de facto standard project manager in Emacs. I know theres project.el, but… Eh… Im used to projectile.

(use-package projectile
  :straight (:build t)
  :diminish projectile-mode
  :custom ((projectile-completion-system 'ivy))
  :init
  (setq projectile-switch-project-action #'projectile-dired)
  :config
  (projectile-mode)
  (add-to-list 'projectile-ignored-projects "~/")
  (add-to-list 'projectile-globally-ignored-directories "^node_modules$")
  :general
  (phundrak/leader-key
    "p" '(:keymap projectile-command-map :which-key "projectile")))

And of course, there is a counsel package dedicated to projectile.

(use-package counsel-projectile
  :straight (:build t)
  :after (counsel projectile)
  :config (counsel-projectile-mode))

Recentf

The built-in package recentf keeps track of recently opened files. But by default, it only follows the twenty most recent files, that not nearly enough for me, so I raise it to two hundred. I also dont want recentf to follow the Elfeed database, so I add it to the list of excluded files.

(use-package recentf
  :straight (:build t :type built-in)
  :custom ((recentf-max-saved-items 2000))
  :config
  (add-all-to-list 'recentf-exclude
                   `(,(rx (* any)
                          (or "elfeed-db"
                              "eln-cache"
                              "conlanging/content"
                              "org/config"
                              "/Mail/Sent"
                              ".cache/")
                          (* any)
                          (? (or "html" "pdf" "tex" "epub")))
                     ,(rx (* any)
                          ".elc"
                          eol)
                     ,(rx "/"
                          (or "rsync" "ssh" "tmp" "yadm" "sudoedit" "sudo")
                          (* any)))))

Screenshot

screenshot.el is a nice utility package made by TEC. It allows the user to take a screenshot of a specific area of a buffer and make it look nice.

(use-package screenshot
  :defer t
  :straight (screenshot :build t
                        :type git
                        :host github
                        :repo "tecosaur/screenshot")
  :config (load-file (locate-library "screenshot.el")))

Shells

Shell-pop

Shell-pop allows the user to easily call for a new shell in a pop-up buffer.

(use-package shell-pop
  :defer t
  :straight (:build t)
  :custom
  (shell-pop-default-directory "/home/phundrak")
  (shell-pop-shell-type (quote ("eshell" "*eshell*" (lambda () (eshell shell-pop-term-shell)))))
  (shell-pop-window-size 30)
  (shell-pop-full-span nil)
  (shell-pop-window-position "bottom")
  (shell-pop-autocd-to-working-dir t)
  (shell-pop-restore-window-configuration t)
  (shell-pop-cleanup-buffer-at-process-exit t))

VTerm

VTerm gives Emacs access to regular shells with an almost regular emulator. Be aware you will most likely need to hit C-c twice to send an interrupt signal.

(use-package vterm
  :defer t
  :straight (:build t)
  :config
  (setq vterm-shell "/usr/bin/fish"
        vterm-always-compile-module t))

One annoying think with vterm is it only can create one buffer, you cant have multiple vterm buffers by default. multi-vterm fixes this issue.

(use-package multi-vterm
  :after vterm
  :defer t
  :straight (:build t)
  :general
  (phundrak/major-leader-key
    :packages '(vterm multi-vterm)
    :keymap 'vterm-mode-map
    "c" #'multi-vterm
    "n" #'multi-vterm-next
    "p" #'multi-vterm-prev))

Another really neat package is eshell-vterm which allows to use vterm to run visual commands when in Eshell.

(use-package eshell-vterm
  :after eshell
  :straight (:build t)
  :config
  (eshell-vterm-mode)
  (defalias 'eshell/v 'eshell-exec-visual))

XWidgets Webkit Browser

I used to use the xwidgets webkit browser in order to view or preview HTML files from Emacs, but it seems the Cairo background transparency patch breaks it. So while this isnt patched, I will disable Xwidgets in my Emacs build, and these keybinds will not be tangled.

(phundrak/evil
 :keymaps 'xwidget-webkit-mode-map
 "<mouse-4>" #'xwidget-webkit-scroll-down-line
 "<mouse-5>" #'xwidget-webkit-scroll-up-line
 "c"         #'xwidget-webkit-scroll-backward
 "t"         #'xwidget-webkit-scroll-up-line
 "s"         #'xwidget-webkit-scroll-down-line
 "r"         #'xwidget-webkit-scroll-forward
 "h"         #'xwidget-webkit-goto-history
 "C"         #'xwidget-webkit-back
 "R"         #'xwidget-webkit-forward
 "C-r"       #'xwidget-webkit-reload
 "j"         nil
 "k"         nil
 "l"         nil
 "H"         nil
 "L"         nil
 "C-d"       #'xwidget-webkit-scroll-up
 "C-u"       #'xwidget-webkit-scroll-down)

Wttr.in

(use-package wttrin
  :defer t
  :straight (wttrin :build t
                    :local-repo "~/fromGIT/emacs-packages/emacs-wttrin"
                    :type git)
                    ;; :host github
                    ;; :repo "Phundrak/emacs-wttrin"
  :config
  (setq wttrin-default-cities '("Aubervilliers" "Paris" "Lyon" "Nonières" "Saint Agrève")
        wttrin-use-metric t))