Skip to content

torgeir/.emacs.d

Repository files navigation

torgeir/.doom.d

Managing a plain emacs config that got closer and closer to what you get ootb with doom emacs was beginning to feel tedious, so this is an attempt to for a setup with ~feature parity on top of doom emacs. It was probably due a clean up after all these years.

Install

First, install doom emacs. I have since moved on to doing this with nix and nix-darwin.

git clone --depth 1 https://github.com/doomemacs/doomemacs ~/.emacs.d
~/.emacs.d/bin/doom install

Then clone the config.

cd && git clone [email protected]:torgeir/.emacs.d.git ~/.doom.d

Put ~/.doom.d/bin on path for the terminal shortcuts e and et to open emacsclient and emacsclient in the terminal.

Doom help

The built in emacs help is fantastic. The doom one on SPC h d is even better! Here’s a few examples.

  • Browse examples of useful doom apis
  • Lookup help for doom modules
  • Search for in the doom .emacs.d folder
  • SPC h d h to access documentation
  • SPC h d followed by C-h invokes a fuzzy search of keys available in the formerly pressed binding. This also works with SPC followed by C-h.
  • K on a module to view its documentation
  • gd on a module to browse its directory

Embark org ToC

Type SPC m g g to run consult-org-heading, then C-c C-; to embark-export.

Emacs variables explained

  • setq sets a variable in the current buffer
  • setq-default sets a default value for a variable for all buffers
  • setq-local sets a buffer local variable, it needs to be buffer local first (make-variable-buffer-local '...)

Troubleshooting doom

When doom sync -u fails for some packages, try deleting the cloned repos before running it again.

E.g. rm -rf ~~/.config/doom-local/straight/repos/{magit,transient,with-editor}

Literate config

This config uses the :config literal that tangles this file to config.el on save. The +literate-config-file is set in init.el and points to this file.

Configuration inspiration

Navigation

I usually navigate this with c-c c-n or c-c c-p, or mgg.

Lexical scoping everywhere

The madness that is dynamic is too much for anyone, really.

;;; $DOOMLOCALDIR/config.el -*- lexical-binding: t; -*-

Packages

This is tangled to packages.el on save.

(package! nerd-icons-dired)
(package! d2-mode)
(package! llm)
(package! ellama)
(package! pulsar)
(package! copilot
  :recipe (:host github :repo "copilot-emacs/copilot.el" :files ("*.el" "dist")))
(package! tide :disable t)
(package! catppuccin-theme)
(package! magit-delta)
(package! elfeed-tube)
(package! evil-cleverparens)
(package! transpose-frame)
(package! highlight-symbol)
(package! olivetti)
(package! calendar-norway)
(package! spray)
(package! dired-subtree)
(package! org-ai :recipe (:host github :repo "rksm/org-ai"))
(package! mastodon :recipe (:host codeberg :repo "martianh/mastodon.el"))
(package! hnreader)
(package! reddigg)
(package! org-appear :recipe (:host github :repo "awth13/org-appear"))
(package! remark-mode)
(package! command-log-mode) ;; C-c o
;; needed for wn program on a mac?
(package! wordnut :pin "feac531404041855312c1a046bde7ea18c674915")
;; needed for wn program on a mac?
(package! synosaurus :pin "14d34fc92a77c3a916b4d58400424c44ae99cd81")
(package! recursion-indicator)
(package! org-alert)
(package! ellama)
(package! spacious-padding)
(package! mu4e-alert)

Try

You can try packages without loading them permanently by calling m-x straight-use-package or (call-interactively ‘straight-use-package).

I can’t remember this, so here’s a function

(defun t/try ()
  (interactive)
  (call-interactively 'straight-use-package))

Recentf

Ignore some of the cached emacs files in recent files

(after! recentf
  (add-to-list 'recentf-exclude "\.emacs\.d/\.local"))

Whoami

(let ((email   (getenv "USER_EMAIL"))
      (email-2 (getenv "USER_EMAIL_2")))
  (when (not email)   (error "No USER_EMAIL set?"))
  (when (not email-2) (error "No USER_EMAIL_2 set?"))
  (setq user-full-name "Torgeir Thoresen"
        user-mail-address   email
        user-mail-address-2 email-2))

1password

(defun t/1p (item &optional args)
  "Lookup 1p item. On linux, sign in manually first."
  (let ((args (or args "--fields label=password")))
    (if is-mac
        (with-temp-buffer
          (if (zerop (call-process-shell-command (format "op item get %s %s" item args) nil t))
              (replace-regexp-in-string (rx "\n" eos) "" (buffer-string))
            (error "1p: looking up item failed.")))
      (let* ((pass (read-passwd "1p master password: "))
             (session-token nil)
             (ret nil))
        (setq session-token (with-temp-buffer
                              (if (zerop (call-process-shell-command (format "echo -n %s | op signin --raw" pass) nil t))
                                  (replace-regexp-in-string (rx "\n" eos) "" (buffer-string))
                                (error "1p: auth failed."))))
        (with-temp-buffer
          (if is-linux
              (call-process-shell-command (format "op --session %s item get %s %s" session-token item args) nil t))
          (replace-regexp-in-string (rx "\n" eos) "" (buffer-string)))))))

Doom env from terminal, including SSH_* and GPG_* env vars

Needed to do this to make emacs discover 1p SSH_AGENT_SOCK set in .zprofile. Or run this command from the terminal

doom env -a ^SSH_ -a ^GPG

gpg

[2023-10-07 Sat] On mac this still needs [email protected] [2024-01-13 Lør] Fixed by patching gnupg

Prerequisits, import and trust key ultimately

gpg --batch --import
# <enter>
# <paste key>
# c-d

gpg --list-keys
gpg --edit-key 922E681804CA8D82F1FAFCB177836712DAEA8B95
# gpg> trust
# gpg> 5
(defun t/gpg ()
  (interactive)
  (start-process-shell-command
   "gpg:agent"
   nil
   (format
    "gpg-connect-agent updatestartuptty /bye > /dev/null && \
       $(gpgconf --list-dirs libexecdir)/gpg-preset-passphrase -c -P '%s' \
       $(gpg --fingerprint --with-keygrip [email protected] | awk '/Keygrip/ {print $3}' | tail -n 1)"
    (t/1p "keybase.io" "--format json | jq -j '.fields[] | select(.id == \"password\") | .value'")))
  (let ((p (start-process-shell-command "gpg:test" nil "gpg -q --batch -d ~/.authinfo.gpg 2>&1 1>/dev/null")))
    (set-process-sentinel p (lambda (p event) (message "%s %s" p event)))))

Org file location setup

(setq org-directory (expand-file-name "~/Dropbox/org/")
      org-agenda-files '("~/Dropbox/org")
      org-archive-location "%s_archive.gpg::") ; so files are encrypted automatically

t-defuns

My old collection of more or less useful defuns.

(progn
  (defconst is-win (featurep :system 'windows))
  (defconst is-cygwin (featurep :system 'windows))
  (defconst is-mac (featurep :system 'macos))
  (defconst is-linux (featurep :system 'linux))
  (defun t/user-file (path) (concat (expand-file-name "~/") path))
  (defun t/user-emacs-file (path) (concat doom-user-dir path))
  (defun t/user-dropbox-folder (path) (expand-file-name (concat "~/Dropbox" (if is-mac " (Personal)" "") "/" path)))
  (load! (concat doom-user-dir "t-defuns.el")))

Micro state

A small overlay map that exposes a set of key bindings until you press q, or something else not in the keymap.

(defun t/micro-state (quit key fn &rest bindings)
  "Micro state that temporarily overlays a new key map, kinda like hydra"
  (let ((keymap (make-sparse-keymap)))
    (while key
      (bind-key key fn keymap)
      (setq key (pop bindings)
            fn (pop bindings)))
    (lambda ()
      (interactive)
      (let ((exit (set-temporary-overlay-map keymap t (lambda () (when quit (quit-window))))))
        (when quit
          (bind-key "q" (cmd! (funcall exit)) keymap))))))

And one that enters a mode, then turns on the keymap. It turns mode off again if you hit a key not in the map.

(defun t/micro-state-in-mode (mode after key fn &rest bindings)
  "Micro state that toggles mode and temporarily overlays a new key map, kinda like hydra"
  (let ((keymap (make-sparse-keymap)))
    (while key
      (bind-key key fn keymap)
      (setq key (pop bindings)
            fn (pop bindings)))
    (lambda ()
      (interactive)
      (funcall mode)
      (set-temporary-overlay-map keymap t (lambda nil
                                            (funcall mode -1)
                                            (when after (after)))))))

Editor

Minibuffers

Some commands are useful from within the minibuffer. This needs enable-recursive-minibuffers, see below.

(after! vertico
  (map! :map (vertico-map
              minibuffer-local-map
              read--expression-map)
        :g "C-k" 'kill-line
        :g "M-SPC" 'doom/leader))

Recursive minibuffers

When you change your mind and need to do something first, after you already started a command that opens the minibuffer. Cancel them with C-].

(setq enable-recursive-minibuffers t)

And a slightly fancier indicator than (minibuffer-depth-indicate-mode)

(use-package! recursion-indicator
  :config
  (recursion-indicator-mode))

Auth sources

Move ~~/.authinfo.gpg~ to the front. It is originally behind the macos keychain that doom puts in there.

(after! auth-source (setq auth-sources (nreverse auth-sources)))

Defaults

(let ((h (* 4 60 60)))
  (setq auth-source-do-cache t
        auth-source-cache-expiry h
        password-cache t
        password-cache-expiry h))

(after! epa
  (setq-default epa-file-encrypt-to '("[email protected]"))
  ;; https://irreal.org/blog/?p=11827
  (fset 'epg-wait-for-status 'ignore))

Wait just long enough.

(setq which-key-idle-delay 0.5
      which-key-idle-secondary-delay 0.05)

Disable annoying defaults

Reset <a href=”file:~/.config/emacs/modules/config/default/config.el::(map! ”” #’drag-stuff-up”>drag stuff on meta arrows, m-left/right is too engrained to move between words.

;; TODO kjører ikke på linux?
(add-hook! 'doom-after-init-hook :append
  (defun t/unbind-drag-stuff ()
    (interactive)
    (map! :g "M-<left>"  nil
          :g "M-<right>" nil)))

On load theme

(defun t/doom-load-theme-hook (&optional &rest _)
  "This is unused atm, no longer needed the highlight indent guides mode stuff."
  (interactive))
(advice-remove 'load-theme 't/doom-load-theme-hook)
(advice-add 'load-theme :after 't/doom-load-theme-hook)

Opt-in to emojis instead 🚀

(add-hook! 'doom-first-buffer-hook
  (defun t/after-first-buffer-hook ()
    (global-emojify-mode -1)))

Soft wrap everywhere

(add-hook! 'doom-after-init-hook
  (defun t/after-init-hook ()
    (setq truncate-lines t)
    (global-visual-line-mode 0)
    (global-hl-line-mode -1)))

Programming modes

(add-hook! '(prog-mode-hook text-mode-hook conf-mode-hook)
  (defun t/prog-mode-hook ()
    (interactive)))

Whitespace

(after! whitespace
  (add-to-list 'whitespace-style 'trailing))
(add-hook!
 '(prog-mode-hook org-mode-hook)
 (defun t/set-whitespace-style ()
   (interactive)
   (setq whitespace-style '(face tabs trailing lines ;; space-mark spaces
                            space-before-tab newline indentation
                            empty space-after-tab tab-mark
                            newline-mark missing-newline-at-eof))))

Emmet

(after! emmet-mode
  (add-to-list 'emmet-jsx-major-modes 'typescript-ts-mode)
  (add-to-list 'emmet-jsx-major-modes 'tsx-ts-mode))

Evil

I spent so much time with vim, I will probably never give it up.

Config

Useful for C-e followed by C-x C-e to eval an s-expression. Makes cleverparens nav commands like L and H move across sexps

(setq evil-move-beyond-eol t)

Don’t use zz and zq for org src editing

(after! evil-collection
  (add-to-list 'evil-collection-key-blacklist "ZZ")
  (add-to-list 'evil-collection-key-blacklist "ZQ"))

Fine undo

(after! evil
  (setq evil-want-fine-undo t))

Indent after paste

(defun t/indent-after-paste (fn &rest args)
  (evil-start-undo-step)
  (let* ((u-prefix (t/prefix-arg-universal?))
         (current-prefix-arg (unless u-prefix current-prefix-arg))
         (args (if u-prefix (list nil) args)))
    (apply fn args)
    (if u-prefix
        (indent-region (region-beginning) (region-end))))
  (evil-end-undo-step))

(advice-add 'yank :around #'t/indent-after-paste)
(advice-add 'evil-paste-before :around #'t/indent-after-paste)
(advice-add 'evil-paste-after :around #'t/indent-after-paste)

Unbind C-h in evil window bindings

I use SPC w h instead of SPC w C-h to move to the left window. C-h is more useful as embark-prefix-help-command, which this falls back to, like in all other keymaps

(map! :after evil :map evil-window-map "C-h" nil)

Try e.g. SPC C-h to browse all available commands with vertico.

Increment & Decrement number

(map! :n "g-" #'evil-numbers/dec-at-pt
      :n "g+" #'evil-numbers/inc-at-pt)

Registers

Some macros I once used.

This one makes camelCaseWords into to snake_case_words. Run it with @c

(evil-set-register ?c [?: ?s ?/ ?\\ ?\( ?\[ ?a ?- ?z ?0 ?- ?9 ?\] ?\\ ?\) ?\\ ?\( ?\[ ?A ?- ?Z ?0 ?- ?9 ?\] ?\\ ?\) ?/ ?\\ ?1 ?_ ?\\ ?l ?\\ ?2 ?/ ?g])

Goggles

(after! evil-goggles
  (setq evil-goggles-duration 0.2
        evil-goggles-enable-delete t
        evil-goggles-enable-change t)
  (evil-goggles-use-diff-refine-faces)
  (pushnew! evil-goggles--commands
            '(evil-cp-delete-line
              :face evil-goggles-delete-face
              :advice evil-goggles--generic-blocking-advice)))

Macros

A useful macro one for testing stuff out

(defmacro comment (&rest ignore)
  nil)

(comment
 (funcall (t/micro-state nil "m" (cmd! (message "1")))))

Macro numbered list

Type qq to record a macro to q. Move to where you want the number and press C-x C-k C-i. Move to the next line start to make the macro repeatble. Type q. Undo. Select the list and hit @q.

  • one
  • two
  • three

Embark

(map!
 :g "C-," #'embark-act ; global
 :map org-mode-map "C-," #'embark-act
 :map minibuffer-mode-map "C-," #'embark-act)

Prevent embark-export, C-e, from being “popupized” by doom’s :ui popup and its (popup +all) setting.

(set-popup-rule! "^*Embark" :ignore t)

You can use C-SPC to preview candidates.

Embark improves prefix help commands, e.g. C-c C-h, by showing auto complete that is fuzzy searchable.

Sometimes its useful not to close it. Hit q after opening it to embark-toggle-quit before e.g. running k to kill a buffer. Or use this with m-x

(after! embark
  (defun embark-act-noquit ()
    "Run action but don't quit the minibuffer afterwards."
    (interactive)
    (let ((embark-quit-after-action nil))
      (embark-act))))

Add a mapping to kill buffers like vterm without all the nagging.

(map! :map embark-buffer-map "D" #'t/volatile-kill-buffer-and-window)

Vertico

C-a c-k is so engrained in my fingers, I need it everywhere. C-a seems to work out of the box.

(after! vertico
  (map! :map vertico-map
        :g "C-k" 'kill-line))

Exclude stuff from +default/search-project by placing excludes in ~/.rgignore

Eldoc

Disable eldoc on the modeline, makes it so eldoc only appears on SPC h ., i.e. on m-x eldoc-doc-buffer

(add-hook! '(web-mode js-mode rjsx-mode typescript-mode typescript-tsx-mode)
  (defun t/eldoc-only-in-buffer ()
    (interactive)
    (setq eldoc-message-function (defun t-void (&optional one two) nil))))

Fix issue where org-eldoc-get-src-lang is not defined?

(add-hook! 'org-mode-hook (defun t/fix-missing-definition-org-eldoc-get-src-lang ()
                            (interactive)
                            (require 'org-eldoc)))

Orderless

A tuned version of Prot’s and Kristoffer Balintona’s vertico, maginalia and orderless setup

Some examples and explanations

m-x: name= ^[m]
contains chars of name in word in order AND starts with regex m
m-x: Buffer= e nm=
contains chars of Buffer in word in order AND contains e AND contains chars of nm in word in order (e.g. like in u<nm>ark)
SPC s p: #defun#j gjp, ha,
rg search for defun, in-emacs matching for long words that have leading inner words starting with g j and p in order, and have leading inner words starting with h and a
(after! orderless

  (setq marginalia-max-relative-age 0)

  (progn

    (setq orderless-matching-styles
          '(orderless-literal
            ;; orderless-initialism
            ;; orderless-regexp
            ;; orderless-flex
            ))

    (setq orderless-style-dispatchers
          '(initialism-dispatcher ;; suffix search with =
            flex-dispatcher       ;; suffix search with .
            regexp-dispatcher     ;; suffix search with ~
            or-regexp             ;; regex search with foo|bar
            ))

    (defun regexp-dispatcher (pattern _index _total)
      "Matches regexp."
      (when (string-suffix-p "~" pattern)
        `(orderless-regexp . ,(substring pattern 0 -1))))

    (defun flex-dispatcher (pattern _index _total)
      "Matches using any group in any order."
      (when (string-suffix-p "." pattern)
        `(orderless-flex . ,(substring pattern 0 -1))))

    (defun or-regexp (pattern index _total)
      "foo|bar"
      (cond
       ((string-suffix-p "|" pattern)
        `(orderless-regexp . ,(concat "\\(" (concat (s-replace "|" "\\|" (substring pattern 0 -1)) "\\)"))))
       ((string-match-p "|" pattern)
        `(orderless-regexp . ,(concat "\\(" (concat (s-replace "|" "\\|" pattern) "\\)"))))))

    (defun literal-dispatcher (pattern _index _total)
      "Literal style dispatcher using the equals sign as a suffix."
      (when (string-suffix-p "=" pattern)
        `(orderless-literal . ,(substring pattern 0 -1))))

    ;;;###autoload
    (defun initialism-dispatcher (pattern _index _total)
      "Matches leading on words in order
E.g.
#fun#gjp, ha,
(defun t/js2-get-json-path (&optional hardcoded-array-index))
 ^^^^^       ^   ^    ^               ^         ^
#fun#gjp, hi,
Would not match the above as no leading words start h then another word starting with i
"
      (when (string-suffix-p "," pattern)
        `(orderless-strict-initialism . ,(substring pattern 0 -1))))

    (defun orderless-strict-initialism (component)
      "Match a COMPONENT as a strict initialism, optionally ANCHORED.
The characters in COMPONENT must occur in the candidate in that
order at the beginning of subsequent words comprised of letters.
Only non-letters can be in between the words that start with the
initials.

If ANCHORED is `start' require that the first initial appear in
the first word of the candidate.  If ANCHORED is `both' require
that the first and last initials appear in the first and last
words of the candidate, respectively."
      (orderless--separated-by
          '(seq (zero-or-more alpha) word-end (zero-or-more (not alpha)))
        (cl-loop for char across component collect `(seq word-start ,char))))))

Editing

Iterate through CamelCase words

(global-subword-mode 1)

+onsave apheleia

npm install -g prettier

The built in apheleia is enough, don’t need eglot formatting as well. It messes up prettier.

(setq +format-with-lsp nil)

Dired

(after! dired
  (setq dired-listing-switches "-aBhl  --group-directories-first")
  (add-hook 'dired-mode-hook (defun t/dired-truncate-lines ()
                               (interactive)
                               (visual-line-mode -1)
                               (toggle-truncate-lines 1)))
  (add-hook 'dired-mode-hook 'nerd-icons-dired-mode)
  (add-hook 'dired-mode-hook 'dired-subtree-toggle)
  (add-hook 'dired-mode-hook 'dired-hide-details-mode)
  (add-hook 'dired-mode-hook 'dired-async-mode)
  )
(defun t/dired-subtree-tab ()
  (interactive)
  (cond
   ((and (t/prefix-arg-universal?)
         (dired-subtree--is-expanded-p)) (t/dired-close-recursively))
   ((t/prefix-arg-universal?) (t/dired-open-recursively))
   (t (t/dired-subtree-toggle))))
(after! (:or dired)
  ;; prevent kill all dired buffers on q
  (map! :map dired-mode-map :ng "q" 't/volatile-kill-buffer)
  (map! :map dired-mode-map :ng "Q" 'evil-record-macro)
  (map!
   :map (dired-mode-map)
   "<return>" (cmd! (if (t/prefix-arg-universal?)
                        (call-interactively 'dired-find-file)
                      (let ((split-window-preferred-function 'ignore))
                        (call-interactively 'dired-find-file))))
   "C-k" 'dired-kill-subdir
   "<tab>" 't/dired-subtree-tab
   :n "<tab>" 't/dired-subtree-tab
   "<backspace>" 'dired-kill-subdir
   "M-<down>" (cmd! (dired-find-alternate-file))
   "M-<up>" (cmd! (find-alternate-file ".."))))

Dired sidebar

(after! dired
  (require 'nerd-icons-dired)
  (advice-add 'dired-subtree-toggle :around #'nerd-icons-dired--refresh-advice))
(defvar t-sidebar-buffer-prefix ":")
;; TODO hackery to be able to tweak display-buffer-alist even with doom's set-popup-rule!
(advice-add #'set-popup-rule! :after
            (defun t/add-display-buffer-alist (fn &rest args)
              (add-to-list 'display-buffer-alist
                           `(,(concat "^" t-sidebar-buffer-prefix)
                             (display-buffer-in-side-window)
                             (side . left)
                             (window-height . fit-window-to-buffer)
                             (body-function . (lambda (window) (set-window-dedicated-p window t)))
                             (window-parameters . ((no-other-window . t)))))))


(defun t-toggle-sidebar ()
  (interactive)
  (let* ((sidebar-project (replace-regexp-in-string (expand-file-name "~") "~" (t/project-root)))
         (sidebar-name (concat t-sidebar-buffer-prefix sidebar-project))
         (sidebar-buffer (get-buffer sidebar-name))
         (sidebar-displayed (and sidebar-buffer (get-buffer-window sidebar-buffer))))
    (if sidebar-displayed
        (delete-window (get-buffer-window sidebar-buffer))
      (when (not sidebar-buffer)
        (with-current-buffer (dired-noselect sidebar-project)
          ;; unadvertise buffer so dired does not consider it on subsequent dired-jum
          (dired-unadvertise (dired-current-directory))
          (rename-buffer sidebar-name)))
      (pop-to-buffer sidebar-name))))
How to clean up display buffer alist entries
(comment
 (setq display-buffer-alist
       (assoc-delete-all "^:" display-buffer-alist))
 )

WIP to locate file in dired

(comment
 (while (not (equal (dired-current-directory) (t/project-root)))
   (progn (dired-up-directory) (dired-subtree-cycle) (revert-buffer))))

Customize

Doom doesnt use the customize interface. It is useful nonetheless for experimenting with face colors etc

(set-popup-rule! "^*Customize" :ignore t)

Make s-s save in customize. Look up the function of a button using describe-text-properties on a button, like the “Apply and Save”

(map! :map custom-mode-map
      "s-s" 'Custom-save)

After consult jump - focus subtree after jumping

Zoom to the previewed org subtree when jumping between headings with consult-org-heading.

(add-hook! 'consult-after-jump-hook :append
  (defun t/after-consult-jump ()
    ""
    ;; org
    (when (eq major-mode 'org-mode)
      (when (org-at-heading-p)
        (outline-hide-sublevels (org-outline-level)))
      (org-show-subtree))

    ;; always
    (recenter)))

Multiple cursors

(after! evil
  (defun t/mc-skip-prev ()
    (interactive)
    (evil-multiedit-toggle-or-restrict-region)
    (evil-multiedit-match-and-prev))

  (defun t/mc-skip-next ()
    (interactive)
    (evil-multiedit-toggle-or-restrict-region)
    (evil-multiedit-match-and-next)))

Make cursor follow matches so m-n or m-p can be used to skip matches easily, depending on what direction you are moving in. R marks all occurrences from visual.

(after! evil
  (setq evil-multiedit-follow-matches t)
  (map!
   :after evil
   :mode evil-multiedit-mode
   ;; for some reason m-j does not work, use m-n and m-p instead
   :n "M-n"   #'t/mc-skip-next
   :n "M-p"   #'t/mc-skip-prev

   ;; don't clash with ~evil-cp-delete-sexp~, require visual mode for multi edit
   :mode emacs-lisp-mode
   :v "M-d" 'evil-multiedit-match-symbol-and-next))

;; test
;; test test
;; test

Restores a lost multiedit selection.

(map!
 :g "C-M-r" 'evil-multiedit-restore)

Multiedit calls iedit which is missing all-caps in emacs 29.

(when (version< "29.0" emacs-version)
  (defun all-caps (smtn)
    (upper smtn)))

Font

(defun t/font-spec (f &optional s weight)
  (font-spec :family f
             :size (or s 20)
             :weight (or weight 'regular)
             :slant 'normal
             :width 'normal))

(setq t-fonts `((:face ,"IosevkaTermCurlySlab Nerd Font")))

(defun t/cycle-fonts (&optional font-spec)
  (interactive)
  (setq t-fonts (nconc (last t-fonts) (butlast t-fonts)))
  (let* ((spec (or font-spec (car t-fonts)))
         (f (plist-get spec :face))
         (s (plist-get spec :size))
         (w (plist-get spec :weight)))
    (message "Font: %s, size: %s, weight: %s" f s w)
    (setq doom-font (t/font-spec f s w)
          doom-variable-pitch-font (t/font-spec "IosevkaEtoile Nerd Font" 19 w)
          doom-big-font (t/font-spec f 28)
          doom-font-increment 2)
    (doom/reload-font)
    f))

(t/cycle-fonts)

Nerd fonts

Remember to run

(nerd-icons-install-fonts)

List available fontsets

(call-interactively 'describe-font)

or

fc-list

Errors

Navigate flymake and flycheck errors

(map!
 :leader
 (:prefix-map ("e" . "errors")
              (:when t
                :desc "Toggle flycheck"        "t" #'flycheck-mode
                :desc "List errors"            "l" (cmd! (cond
                                                          ((and (boundp 'flycheck-mode) flycheck-mode) (flycheck-list-errors))
                                                          (t (flymake-show-buffer-diagnostics))))
                :desc "Jump to next error"     "n" (cmd! (cond
                                                          ((and (boundp 'flycheck-mode) flycheck-mode) (flycheck-next-error))
                                                          (t (flymake-goto-next-error))))
                :desc "Jump to previous error" "N" (cmd! (cond
                                                          ((and (boundp 'flycheck-mode) flycheck-mode) (flycheck-previous-error))
                                                          (t (flymake-goto-prev-error)))))))

Skip to flymake issues when skipping through them

(after! flymake
  (map!
   :map flymake-diagnostics-buffer-mode-map
   :n "C-p" (cmd! (let ((p (point))
                        (b (current-buffer)))
                    (previous-line)
                    (flymake-goto-diagnostic (point))
                    (pop-to-buffer b)
                    (goto-char p)
                    (previous-line) ;; again??
                    ))
   :n "C-n" (cmd! (let ((p (point))
                        (b (current-buffer)))
                    (next-line)
                    (flymake-goto-diagnostic (point))
                    (pop-to-buffer b)
                    (goto-char p)
                    (next-line) ;; again??
                    ))))

Eglot flycheck issue

doomemacs/doomemacs#6466

(after! (eglot flycheck)
  (push 'eglot flycheck-checkers)
  (delq! 'eglot flycheck-checkers))

Projects

Ignore some extra folders from projectile

(after! projectile
  (add-to-list 'projectile-globally-ignored-directories "^build$")
  (add-to-list 'projectile-globally-ignored-directories "^target$")
  (add-to-list 'projectile-globally-ignored-directories "^\\.log$"))

Workspaces

(map!
 :leader "1" '+workspace/switch-to-0
 :leader "2" '+workspace/switch-to-1
 :leader "3" '+workspace/switch-to-2
 :leader "4" '+workspace/switch-to-3
 :leader "5" '+workspace/switch-to-4
 :leader "6" '+workspace/switch-to-5
 :leader "7" '+workspace/switch-to-6
 :leader "8" '+workspace/switch-to-7
 :leader "9" '+workspace/switch-to-8
 :leader "0" '+workspace/switch-to-final
 :leader "-" '+workspace/switch-to)

And fix super navigation across modes that steal SPC.

(map!
 "s-1" '+workspace/switch-to-0
 "s-2" '+workspace/switch-to-1
 "s-3" '+workspace/switch-to-2
 "s-4" '+workspace/switch-to-3
 "s-5" '+workspace/switch-to-4
 "s-6" '+workspace/switch-to-5
 "s-7" '+workspace/switch-to-6
 "s-8" '+workspace/switch-to-7
 "s-9" '+workspace/switch-to-8
 "s-0" 'doom/reset-font-size)

Be explicit about when deleting workspaces

(after! (:and evil persp-mode)
  (define-key! persp-mode-map
    [remap delete-window] #'delete-window
    [remap evil-window-delete] #'delete-window))

(map!
 :map doom-leader-workspace-map
 :leader :desc "Other workspace" "TAB '" '+workspace/other
 :leader :desc "New workspace" "TAB w" '+workspace/new-named
 :leader :desc "Next workspace" "TAB n" '+workspace:switch-next
 :leader :desc "Previous workspace" "TAB p" '+workspace:switch-previous
 :leader :desc "Swap next" "TAB j" '+workspace/swap-right
 :leader :desc "Swap previous" "TAB k" '+workspace/swap-left)

;; like tmux window nav
(map!
 ;; make room under c-b
 :gnm "C-b" nil
 :gm :desc "Next workspace" "C-b C-n" '+workspace:switch-next
 :gm :desc "Previous workspace" "C-b C-p" '+workspace:switch-previous
 :map (magit-mode-map vterm-mode-map)
 :gnm "C-b" nil
 :gn :desc "Next workspace" "C-b C-n" '+workspace:switch-next
 :gn :desc "Previous workspace" "C-b C-p" '+workspace:switch-previous)

(map!
 :desc "Goto workspace" "s-t" '+workspace/switch-to
 :desc "Rename workspace" "s-r" '+workspace/rename)

Company

Make tab accept the current suggestion.

(after! company
  (map! :map company-active-map
        "<tab>" 'company-complete-selection
        ;; and c-e and right arrow like in zsh-autosuggest
        "C-e" 'company-complete-selection
        "<right>" 'company-complete-selection))

Tramp

(after! tramp

  (setq tramp-default-method "ssh"
        tramp-verbose 1
        tramp-default-remote-shell "/bin/bash"
        tramp-connection-local-default-shell-variables
        '((shell-file-name . "/bin/bash")
          (shell-command-switch . "-c")))

  (connection-local-set-profile-variables 'tramp-connection-local-default-shell-profile
                                          '((shell-file-name . "/bin/bash")
                                            (shell-command-switch . "-c"))))

Recentf cleanup logs a lot of error messages, like described here

(after! tramp
  ;; https://discourse.doomemacs.org/t/recentf-cleanup-logs-a-lot-of-error-messages/3273/4
  (advice-add 'doom--recentf-file-truename-fn :override
              (defun my-recent-truename (file &rest _args)
                (if (or (not (file-remote-p file)) (equal "sudo" (file-remote-p file 'method)))
                    (abbreviate-file-name (file-truename (tramp-file-local-name file)))
                  file))))

Editorconfig is extremely slow, e.g. when using doom/sudo-find-file to open, say, /etc/systemd/system/. This fixes that.

(after! tramp
  (setq tramp-ignored-file-name-regexp ".editorconfig"))

Github Codespaces

Add for Github codespaces over ssh, for tramp editing, e.g. with C-x C-f /ghcs:codespace-name:/path/to/file

Thanks to https://blog.sumtypeofway.com/posts/emacs-config.html for this one

(after! tramp
  (let ((ghcs (assoc "ghcs" tramp-methods))
        (ghcs-methods '((tramp-login-program "gh")
                        (tramp-login-args (("codespace") ("ssh") ("-c") ("%h")))
                        (tramp-remote-shell "/bin/sh")
                        (tramp-remote-shell-login ("-l"))
                        (tramp-remote-shell-args ("-c")))))
    ;; just for debugging the methods
    (if ghcs (setcdr ghcs ghcs-methods)
      (push (cons "ghcs" ghcs-methods) tramp-methods))))

The above needs the following feature in the codespace

{
    "features": {
        "ghcr.io/devcontainers/features/sshd:1": {
            "version": "latest"
        }
    }
}

Themes

There’s a lot of good doom themes. I tuned doom-one a little, darkening some of the colors even more. Its in themes/t-doom-one-theme.el.

(setq *t-themes* '(doom-feather-dark
                   doom-flatwhite
                   t-doom-one
                   catppuccin
                   doom-vibrant)
      doom-theme (car *t-themes*)
      t-system-theme-dark 't-doom-one
      t-system-theme-dark 'catppuccin
      t-system-theme-light 'doom-flatwhite)

Cycle through nice ones.

(defun t/cycle-theme ()
  "Cycle through the themes of `*t-themes*`."
  (interactive)
  (setq *t-themes*
        (if (t/prefix-arg-universal?)
            (append (list (car (reverse *t-themes*))) (butlast *t-themes*))
          (append (cdr *t-themes*) (list (car *t-themes*)))))
  (let ((theme (car *t-themes*)))
    (load-theme theme t)
    (setq doom-theme theme)
    (message "Theme: %s" theme)))

Bind it to SPC t t. To cycle the other way around do SPC u SPC t t

(map! :leader "t t" #'t/cycle-theme)

And cycle between the selected t-system-theme-dark and t-system-theme-light when the system appearance is changed on macos.

(advice-remove 't/toggle-system-appearence :after)
(advice-add 't/toggle-system-appearence :after 't/load-system-theme)

Line numbers

Determines the style of line numbers in effect. If set to nil, line numbers are disabled. For relative line numbers, set this to relative. Off by default, relative in programming modes. Toggle them with SPC t l.

(setq display-line-numbers-type nil)
(setq-hook! 'prog-mode-hook display-line-numbers-type 'relative)

Set across all real buffers.

(comment
 (progn
   (t/in-all-buffers (lambda (b) (setq display-line-numbers 'relative)))
   (t/in-all-buffers (lambda (b) (setq display-line-numbers nil)))))

Rainbow mode

Rainbow mode in prog modes
(add-hook! '(prog-mode-hook css-mode-hook html-mode-hook) 'rainbow-mode)
(add-hook! '(prog-mode-hook css-mode-hook html-mode-hook) 'show-paren-mode)
Color parens uniformly
(custom-set-faces!
  '(show-paren-match :background nil :foreground "yellow" :weight bold)
  '(rainbow-delimiters-depth-1-face :foreground "DeepPink4" :overline nil :underline nil)
  '(rainbow-delimiters-depth-2-face :foreground "DeepPink3" :overline nil :underline nil)
  '(rainbow-delimiters-depth-3-face :foreground "DeepPink2" :overline nil :underline nil)
  '(rainbow-delimiters-depth-4-face :foreground "DeepPink1" :overline nil :underline nil)
  '(rainbow-delimiters-depth-5-face :foreground "maroon4" :overline nil :underline nil)
  '(rainbow-delimiters-depth-6-face :foreground "maroon3" :overline nil :underline nil)
  '(rainbow-delimiters-depth-7-face :foreground "maroon2" :overline nil :underline nil)
  '(rainbow-delimiters-depth-8-face :foreground "maroon1" :overline nil :underline nil)
  '(rainbow-delimiters-depth-9-face :foreground "VioletRed3" :overline nil :underline nil)
  '(rainbow-delimiters-depth-10-face :foreground "VioletRed2" :overline nil :underline nil)
  '(rainbow-delimiters-depth-11-face :foreground "VioletRed1" :overline nil :underline nil)
  '(rainbow-delimiters-unmatched-face :foreground "Red" :overline nil :underline nil))

Transparency

(let ((tr 99))
  (t/transparency tr)
  (comment
   (advice-remove #'load-theme :after)
   (advice-remove #'load-theme :before)
   )
  (advice-add #'doom/reload-theme :after (cmd! (t/transparency tr))))

Frame

Show the buffer and the file

(setq frame-title-format "%b (%f)")

Windows

Spacious-padding for more space

(use-package! spacious-padding
  :defer t
  :config (spacious-padding-mode))

Scroll mru window

Scroll most recently used window when using c-m-v and c-m-S-v.

(setq other-window-scroll-default #'get-lru-window)

Split windows manually

If say a single dired window is visible and it is dedicated, allow splitting, else never allow splitting.

(setq split-window-preferred-function 'split-window-sensibly)
;;(setq split-window-preferred-function
;;      (lambda (ignored-window)
;;        (if (= 1 (length (window-list)))
;;            (split-window-right)
;;          nil)))

Resize window combinations proportionally

(setq-default window-combination-resize t)

Resize using arrow keys

If there is no window in the direction you move, send the keypress for the direction instead hjkl.

(map! :after evil
      :map evil-window-map
      "s" (t/micro-state
           nil
           "<left>" (cmd! (cond
                           ((and (window-in-direction 'right) (window-in-direction 'left)) (evil-resize-window (- (window-width) 8) t))
                           ((window-in-direction 'left) (evil-resize-window (+ (window-width) 8) t))
                           ((window-in-direction 'right) (evil-resize-window (- (window-width) 8) t))
                           (t (execute-kbd-macro "h"))))
           "<right>" (cmd! (cond
                            ((and (window-in-direction 'right) (window-in-direction 'left)) (evil-resize-window (+ (window-width) 8) t))
                            ((window-in-direction 'right) (evil-resize-window (+ (window-width) 8) t))
                            ((window-in-direction 'left) (evil-resize-window (- (window-width) 8) t))
                            (t (execute-kbd-macro "l"))))
           "<up>" (cmd! (cond
                         ((and (window-in-direction 'up) (window-in-direction 'down)) (evil-resize-window (+ (window-height) 4)))
                         ((window-in-direction 'down) (evil-resize-window (- (window-height) 4)))
                         ((window-in-direction 'up) (evil-resize-window (+ (window-height) 4)))
                         (t (execute-kbd-macro "k"))))
           "<down>" (cmd! (cond
                           ((and (window-in-direction 'up) (window-in-direction 'down)) (evil-resize-window (- (window-height) 4)))
                           ((window-in-direction 'up) (evil-resize-window (- (window-height) 4)))
                           ((window-in-direction 'down) (evil-resize-window (+ (window-height) 4)))
                           (t (execute-kbd-macro "j"))))))

Messages

;; TODO

(defvar +messages--auto-tail-enabled nil)

(defun +messages--auto-tail-a (&rest arg)
  "Make *Messages* buffer auto-scroll to the end after each message."
  (let* ((buf-name (buffer-name (messages-buffer)))
         ;; Create *Messages* buffer if it does not exist
         (buf (get-buffer-create buf-name)))
    ;; Activate this advice only if the point is _not_ in the *Messages* buffer
    ;; to begin with. This condition is required; otherwise you will not be
    ;; able to use `isearch' and other stuff within the *Messages* buffer as
    ;; the point will keep moving to the end of buffer :P
    (when (not (string= buf-name (buffer-name)))
      ;; Go to the end of buffer in all *Messages* buffer windows that are
      ;; *live* (`get-buffer-window-list' returns a list of only live windows).
      (dolist (win (get-buffer-window-list buf-name nil :all-frames))
        (with-selected-window win
          (goto-char (point-max))))
      ;; Go to the end of the *Messages* buffer even if it is not in one of
      ;; the live windows.
      (with-current-buffer buf
        (goto-char (point-max))))))

(defun +messages-auto-tail-toggle ()
  "Auto tail the '*Messages*' buffer."
  (interactive)
  (if +messages--auto-tail-enabled
      (progn
        (advice-remove 'message '+messages--auto-tail-a)
        (setq +messages--auto-tail-enabled nil)
        (message "+messages-auto-tail: Disabled."))
    (advice-add 'message :after '+messages--auto-tail-a)
    (setq +messages--auto-tail-enabled t)
    (message "+messages-auto-tail: Enabled.")))

Jump around

Some of these, like SPC j c works across windows when prefixed with C-u or SPC u.

(map!
 :leader
 (:prefix-map ("j" . "jump")
              (:when t
                :desc "Jump to window"      "W" #'ace-window
                :desc "Jump to word"        "w" #'avy-goto-word-1
                :desc "Jump to line"        "l" #'avy-goto-line
                :desc "org: Jump to header" "h" #'avy-org-goto-heading-timer
                :desc "Jump to char"        "c" #'avy-goto-char-2
                :desc "Jump to char"        "C" #'avy-goto-char)))

Avy tweaks

(after! (avy evil-integration)
  (defun t/setup-avy (&optional frame)
    (interactive)
    (setq avy-keys '(?j ?f ?d ?k ?s ?a)
          avy-timeout-seconds 0.2
          ;;avy-all-windows 'all-frames
          avy-all-windows nil
          avy-case-fold-search nil
          avy-highlight-first t
          avy-style 'at-full
          avy-background t)
    (let* ((b "#222") (f "DeepPink1"))
      (set-face-attribute 'avy-background-face nil :foreground b)
      (set-face-attribute 'avy-lead-face   nil :background b :foreground f :weight 'bold)
      (set-face-attribute 'avy-lead-face-0 nil :background b :foreground f :weight 'bold)
      (set-face-attribute 'avy-lead-face-1 nil :background b :foreground f :weight 'bold)
      (set-face-attribute 'avy-lead-face-2 nil :background b :foreground f :weight 'bold)))

  (t/setup-avy)

  ;;Also after creating a new frame when emacs is in daemon mode
  (add-hook! 'doom-load-theme-hook :append #'t/setup-avy))

Smartparens

Use paredit bindings. Make `' a pair in emacs lisp mode.

(after! smartparens
  (sp-local-pair 'emacs-lisp-mode "`" "'" :when '(sp-in-docstring-p))
  (add-hook! (clojure-mode emacs-lisp-mode cider-repl-mode) :append #'smartparens-strict-mode)
  (sp-use-paredit-bindings))

And add some extra pairs for org mode.

(after! smartparens
  (sp-with-modes 'org-mode
    (sp-local-pair "`" "'" :when '(sp-in-docstring-p))
    (sp-local-pair "*" "*" :actions '(insert wrap) :unless '(sp-point-after-word-p sp-point-at-bol-p) :wrap "C-*" :skip-match 'sp--org-skip-asterisk)
    (sp-local-pair "_" "_" :unless '(sp-point-after-word-p) :post-handlers '(("[d1]" "SPC")))
    (sp-local-pair "/" "/" :unless '(sp-point-after-word-p) :post-handlers '(("[d1]" "SPC")))
    (sp-local-pair "~" "~" :unless '(sp-point-after-word-p) :post-handlers '(("[d1]" "SPC")))
    (sp-local-pair "<" ">" :unless '(sp-point-after-word-p) :post-handlers '(("[d1]" "SPC")))
    (sp-local-pair "=" "=" :unless '(sp-point-after-word-p) :post-handlers '(("[d1]" "SPC")))
    (sp-local-pair "«" "»")))

Smartparens-mode paredit bindings in org mode messes up M-up and M-down, bring them back.

(add-hook! 'org-mode-hook
  (defun t/org-mode-hook ()
    (map!
     :map evil-motion-state-local-map
     "M-<up>"    'org-metaup
     "M-<down>"  'org-metadown
     "M-S-<right>" 'org-shiftmetaright
     "M-S-<left>" 'org-shiftmetaleft)))

Don’t create cache files

(add-hook! 'org-mode-hook (defun t/org-disable-auto-save-mode () (interactive) (auto-save-mode -1)))

Bring back C-k in the minibuffer. Overrides +evil-bindings.el.

(map! :map (evil-ex-completion-map evil-ex-search-keymap)
      :gi "C-k" #'kill-line)
(define-key!
  :keymaps +default-minibuffer-maps
  "C-k" #'kill-line)

Wrap around

Support wrapping sexps by holding super, both in normal mode and insert mode, from the front and the back of expressions.

(map! :map smartparens-mode-map
      ;; literally S-s-8 on a norwegian mac keyboard
      :n "s-(" (cmd! (evil-emacs-state nil)
                     (sp-wrap-with-pair "\(")
                     (evil-normal-state nil))
      :i "s-(" (cmd! (sp-wrap-with-pair "\("))

      ;; literally S-s-MetaRight-8 on my norwegian mac keyboard
      :n "s-{" (cmd! (evil-emacs-state nil)
                     (sp-wrap-with-pair "\{")
                     (evil-normal-state nil))
      :i "s-{" (cmd! (sp-wrap-with-pair "\{"))

      ;; literally S-MetaRight-8 on my norwegian mac keyboard
      :n "s-[" (cmd! (evil-emacs-state nil)
                     (sp-wrap-with-pair "\[")
                     (evil-normal-state nil))
      :i "s-[" (cmd! (sp-wrap-with-pair "\["))

      ;; literally S-s-9 on a norwegian mac keyboard
      :n "s-)" (cmd! (evil-emacs-state nil)
                     (backward-sexp)
                     (sp-wrap-with-pair "\(")
                     (forward-sexp)
                     (evil-normal-state nil))
      :i "s-)" (cmd! (backward-sexp)
                     (sp-wrap-with-pair "(")
                     (forward-sexp))

      ;; literally S-s-MetaRight-9 on my norwegian mac keyboard
      :n "s-}" (cmd! (evil-emacs-state nil)
                     (backward-sexp)
                     (sp-wrap-with-pair "\{")
                     (forward-sexp)
                     (evil-normal-state nil))
      :i "s-}" (cmd! (backward-sexp)
                     (sp-wrap-with-pair "\{")
                     (forward-sexp))

      ;; literally S-MetaRight-9 on my norwegian mac keyboard
      :n "s-]" (cmd! (evil-emacs-state nil)
                     (backward-sexp)
                     (sp-wrap-with-pair "\[")
                     (forward-sexp)
                     (evil-normal-state nil))
      :i "s-]" (cmd! (backward-sexp)
                     (sp-wrap-with-pair "\[")
                     (forward-sexp)))

Expand braces

[[file:~/.config/emacs/modules/config/default/config.el::dolist (brace ‘(“(” “{” “\[“)][Override this to always expand braces]].

(after! smartparens
  (sp-pair "{" nil :post-handlers '(("||\n[i]" "RET") ("| " " ")))
  (sp-pair "(" nil :post-handlers '(("||\n[i]" "RET") ("| " " ")))
  (sp-pair "[" nil :post-handlers '(("||\n[i]" "RET"))))

Distraction free / Zen

A really global global writeroom mode. The function is redefined such that if writeroom-major-modes is nil, writeroom-mode is activated in ALL buffers.

(setq writeroom-major-modes nil)
(defun turn-on-writeroom-mode ()
  (interactive)
  (when (or (not writeroom-major-modes)
            (apply 'derived-mode-p writeroom-major-modes))
    (writeroom-mode 1)))

The doom default text scale of 2 is a bit heavy

(setq +zen-text-scale 0)

Bring back text zoom in writeroom mode, moving away toggle mode-line, awkwardly bound to s-?. Give it an even more awkward binding.

(map! :map writeroom-mode-map
      "s-?" (cmd! (text-scale-increase 1))
      "s-:" 'writeroom-toggle-mode-line)

And screens are big, so a bit more space for text is nice.

(defun t/sidebar-frac (&optional ignore)
  (let* ((w-px (frame-pixel-width (selected-frame)))
         (h-px (frame-pixel-height (selected-frame)))
         (w (frame-width (selected-frame))))
    ;; noisy
    ;; (message "w: %s, w-px: %s, h-px: %s" w w-px h-px)
    (cond
     ((< w-px h-px) (/ (float 1) 3))
     ((> w 200) (/ (float 2) 5))
     ((and (> w 160) (> w-px 1440)) (/ (float 3) 7))
     (t (/ (float 2) 5)))))
(after! writeroom-mode
  (setq writeroom-width (t/sidebar-frac)))
(after! olivetti
  (setq olivetti-minimum-body-width 90)
  (setq-default olivetti-body-width (floor (* (frame-width (selected-frame)) (t/sidebar-frac))))
  (add-to-list 'window-size-change-functions 'olivetti-set-window t))

Adjust margins equally across modes.

(map! :map evil-window-map
      "M" (t/micro-state
           nil
           "<left>" (cmd! (cond
                           ((and (boundp 'writeroom-mode) writeroom-mode) (writeroom-decrease-width))
                           ((and (boundp 'olivetti-mode) olivetti-mode) (olivetti-shrink))
                           (t (t/margins-global-decrease))))
           "<right>" (cmd! (cond
                            ((and (boundp 'writeroom-mode) writeroom-mode) (writeroom-increase-width))
                            ((and (boundp 'olivetti-mode) olivetti-mode) (olivetti-expand))
                            (t (t/margins-global-increase))))))

Modeline

Show workspace in modeline, adjust bar width, moar iconz, truncate path.

(defun t/doom-modeline-mode-hook (&optional &rest ignore)
  (interactive)
  (setq doom-modeline-persp-name t
        doom-modeline-persp-icon t
        ;; doom-modeline-height (* 2 (font-get (or (and doom-big-font-mode doom-big-font) doom-font) :size))
        ;; doom-feather-dark-padded-modeline t
        doom-themes-padded-modeline t
        doom-modeline-bar-width 4
        doom-modeline-github t
        doom-modeline-repl t
        doom-modeline-battery t
        display-time-24hr-format t
        ;; it needs padding to the right
        display-time-string-forms '(dayname " " day "/" month "    ")
        doom-modeline-major-mode-icon t
        doom-modeline-major-mode-color-icon t
        doom-modeline-buffer-file-name-style 'truncate-upto-root)
  (use-package! mu4e-alert
    :after mu4e
    :init (setq doom-modeline-mu4e nil)
    :config (mu4e-alert-enable-mode-line-display))
  (after! doom-modeline
    (set-face-attribute 'doom-modeline-persp-name nil :foreground "DeepPink2" :weight 'bold :italic nil)
    (display-battery-mode)
    (display-time-mode)
    (doom-modeline-github-timer)))
(t/doom-modeline-mode-hook)
(add-hook! 'doom-load-theme-hook :append #'t/doom-modeline-mode-hook)

Doom modeline customization

Read more on seagle0128/doom-modeline

Get a modeline
(doom-modeline 'main)
Create a modeline

You could add your own segments to something like this.

(doom-modeline-def-modeline 't-modeline
  '(bar window-number modals matches buffer-info-simple)
  '(media-info major-mode time))

Running it creates the function

(doom-modeline-format--t-modeline)
Set the modeline
(doom-modeline-set-modeline 't-modeline)
This sets buffer-local mode-line-format to show it
mode-line-format

To set it by default (setf (default-value 'mode-line-format) ...) is used

Create your own segment

(after! doom-modeline
  (doom-modeline-def-segment tasks
    "Display # of tasks not refiled. Use (nerd-icons-insert-faicon) to look up icons."
    (concat
     (doom-modeline-spc)
     (when-let ((icon (doom-modeline-icon 'faicon "nf-fae-checklist_o" "🗉" "")))
       (concat
        (doom-modeline-display-icon icon)
        (doom-modeline-vspc)
        (with-current-buffer "tasks.org"
          (let ((count 0))
            ;; for each heading
            (org-map-entries
             (lambda (&optional heading)
               (when (not (org-entry-is-done-p))
                 (setq count (1+ count))))
             ;; all headline
             t
             ;; in file
             'file)
            ;; needs to be string
            (format "%s" count)))
        (doom-modeline-vspc)
        )))))

Extend the doom ‘main default one, by advicing it

It has 3 parts, the left, the separator and the right.

(defun t/around-doom-modeline-format--main (fn)
  (interactive)
  (let ((res (funcall fn)))
    (list
     (nth 0 res)
     (nth 1 res)
     (cons '(:eval (doom-modeline-segment--tasks))
           (nth 2 res)))))

(advice-remove 'doom-modeline-format--main 't/around-doom-modeline-format--main)
(advice-add 'doom-modeline-format--main :around 't/around-doom-modeline-format--main)

Dictionary

Fix +lookup/dictionary-definition so that it adheres to display-buffer-alist.

(set-popup-rule! "^\\*osx-dictionary" :side 'right :size 0.5 :vslot 2)
(setq osx-dictionary-generate-buffer-name-function
      (lambda (&rest args)
        (pop-to-buffer osx-dictionary-buffer-name)
        osx-dictionary-buffer-name))

REPLs

(after! ielm
  (add-hook 'inferior-emacs-lisp-mode-hook 'evil-cleverparens-mode))

Dotfiles

Highlight dotfiles that are sourced from the shell in sh-mode based on their file location.

(add-to-list 'auto-mode-alist (cons (concat "^" (t/user-file "dotfiles") "/" "[^.]") 'sh-mode))
(add-to-list 'auto-mode-alist (cons (concat "^" (t/user-file "Projects/dotfiles") "/" "[^.]") 'sh-mode))

Keybindings

  • Doom editor keybindings
  • +evil-bindings.el
  • evil commands
(map! :after markdown-mode
      :map evil-markdown-mode-map
      :i "M-b" nil
      :map markdown-mode-map
      :i "M-b" 'backward-word
      :i "M-f" 'forward-word
      "M-p" 'backward-paragraph
      "M-n" 'forward-paragraph)
(map!
 ;; resize fonts
 :n "s-0" nil
 :g "s-0" #'doom/reset-font-size
 :g "s-+" #'doom/increase-font-size
 :g "s--" #'doom/decrease-font-size
 :n "C-+" (cmd! (text-scale-increase 1))
 :n "C--" (cmd! (text-scale-decrease 1))

 ;; and on linux?
 "s-?" (cmd! (text-scale-increase 1))
 "s-_" (cmd! (text-scale-decrease 1))
 "s-=" (cmd! (text-scale-set 0))

 ;; split windows
 "s-d" #'t/split-window-right-and-move-there-dammit
 "s-D" #'t/split-window-below-and-move-there-dammit

 ;; move around with opt+cmd, like in ye olde iterm
 "s-M-<up>" 'evil-window-up
 "s-M-<right>" 'evil-window-right
 "s-M-<down>" 'evil-window-down
 "s-M-<left>" 'evil-window-left

 ;; resize frame
 "C-s-<left>" 't/decrease-frame-width
 "C-s-<right>" 't/increase-frame-width
 "C-s-<down>" 't/increase-frame-height
 "C-s-<up>" 't/decrease-frame-height

 ;; move like history in the terminal
 "M-n" 'forward-paragraph
 "M-p" 'backward-paragraph

 ;; g = global
 :g "M-y" 'consult-yank-from-kill-ring

 ;; i = insert
 :i "C-d" #'delete-char
 :i "C-k" #'evil-delete-line
 :i "C-p" #'previous-line
 :i "C-n" #'next-line

 ;; mark all like on macos
 "s-a" 'mark-whole-buffer

 ;; skip between buffers
 "s-k" 'previous-buffer
 "s-j" 'next-buffer

 ;; skip between windows like on macos
 "s->" 'next-multiframe-window
 "s-<" 'previous-multiframe-window

 ;; beginning and end of line like macos
 "s-<left>" 't/smart-beginning-of-line
 "s-<right>" 'end-of-line

 ;; complete with similar words in buffer
 "C-." 't/hippie-expand-no-case-fold

 ;; beginning
 "C-a" 't/smart-beginning-of-line

 ;; m = motion
 :m "C-e" 'end-of-line

 ;; more file commands like on macos
 "s-q" 'save-buffers-kill-emacs
 "s-n" 'make-frame
 "s-s" 'save-buffer
 "s-w" #'t/delete-frame-or-hide-last-remaining-frame

 ;; op -- :leader :desc "Toggle treemacs" "f L" #'+treemacs/toggle
 :leader :desc "Open folder" "p o" #'t/open-in-desktop

 :leader :desc "Toggle directory sidebar" "f l" #'t-toggle-sidebar
 :leader :desc "Toggle directory sidebar, follow" "f L" 't/dired-locate
 :leader (:prefix ("o" . "open")
                  (:prefix-map
                   ("c" . "Consume")
                   (:when t
                     :desc "nrk.no" "n" (cmd! (t/eww-readable "https://www.nrk.no/nyheter/" 't/clean-nrk-buffer))
                     :desc "hackernews"  "h" (cmd! (+workspace-switch "*hn*" t)
                                                   (hnreader-news))
                     :desc "rss"         "r" #'=rss
                     :desc "mail"        "m" (cmd! (t/gpg) (=mu4e))
                     :desc "music"       "M" (cmd! (+workspace-switch "*emms*")
                                                   (emms-cache-set-from-mpd-all)
                                                   (emms-smart-browse))
                     :desc "mastodon"    "d" (cmd! (+workspace-switch "*mastodon*" t)
                                                   (mastodon))
                     :desc "gnus" "g" (cmd! (+workspace-switch "*gnus*" t)
                                            (gnus)))))
 :leader :desc "Calendar"          "o C" #'calendar
 :leader :desc "Browse"            "o e" #'eww
 :leader :desc "Www"               "o w" #'eww
 :leader :desc "Music"             "o m" (t/micro-state
                                          nil
                                          "+" 't/music-volume-up
                                          "-" 't/music-volume-down
                                          "H" 't/music-prev
                                          "h" 't/music-seek-backward
                                          "l" 't/music-seek-forward
                                          "L" 't/music-next
                                          "p" 't/music-play-pause
                                          "b" 't/music-browse
                                          "s" 't/music-stop)
 :leader :desc "Show home"         "o h" #'(lambda () (interactive) (find-file (t/user-dropbox-folder "org/home.org.gpg")))
 :leader :desc "Show da"           "o d" #'(lambda () (interactive) (find-file (t/user-dropbox-folder "org/da.org.gpg")))
 :leader :desc "Open Intellij"     "o i" #'t/open-in-intellij
 :leader :desc "Browse at point"   "o b" #'t/browse-url-at-point
 :leader :desc "Browse chrome url" "o B" #'t/browse-chrome-url-in-eww

 :leader :desc "Search the web"   "s w" #'consult-web-search
 :leader :desc "Search marks"     "s M" #'evil-show-marks
 :leader :desc "Search registers" "s R" #'evil-show-registers

 :leader :desc "Toggle copilot"        "t c" #'copilot-mode
 :leader :desc "Fill column indicator" "t C" #'display-fill-column-indicator-mode
 :leader :desc "Toggle Big mode"       "t B" #'doom-big-font-mode
 :leader :desc "Toggle dedication"     "t d" #'t/toggle-dedicated-window
 :leader :desc "Toggle emoji"          "t e" #'global-emojify-mode ; :rocket:
 :leader :desc "Debug on error"        "t D" #'toggle-debug-on-error
 :leader :desc "Cycle fonts"           "t f" #'t/cycle-fonts
 :leader :desc "Toggle focus mode"     "t F" #'focus-mode
 :leader :desc "Toggle idle highlight" "t h" #'t-idle-highlight-mode
 :leader :desc "Toggle highlight line" "t H" #'hl-line-mode
 :leader :desc "Toggle variable pitch" "t v" (defun t/variable-pitch-mode (&optional turn-on)
                                               "https://www.reddit.com/r/DoomEmacs/comments/l9jy0h/how_does_variablepitchmode_work_and_why_does_it/."
                                               (interactive)
                                               (if (or turn-on (derived-mode-p 'solaire-mode))
                                                   (progn
                                                     (solaire-mode -1)
                                                     (variable-pitch-mode 1))
                                                 (progn
                                                   (variable-pitch-mode nil)
                                                   (call-interactively 'solaire-mode))))
 :leader :desc "Toggle visual linemode""t V" #'visual-line-mode
 :leader :desc "Toggle truncate"       "t u" #'toggle-truncate-lines
 :leader :desc "Toggle margins"        "t M" #'t/margins-global
 :leader :desc "Toggle olivetti"       "t o" #'olivetti-mode
 :leader :desc "Toggle transparency"   "t T" #'t/transparency
 :leader :desc "Reading"               "r" #'t/start-spray-micro-state
 :leader :desc "Show whitespace"       "t w" #'whitespace-mode
 :leader :desc "Toggle writeroom"      "t z" #'global-writeroom-mode

 :leader :desc "Flip frame"                     "w f" #'rotate-frame
 :leader :desc "Delete window or frame or hide" "w d" #'t/delete-window-or-frame-or-hide
 :leader :desc "Delete buffer and window"       "w K" #'t/volatile-kill-buffer-and-window
 :leader :desc "Winner redo"                  "w R" #'winner-redo
 :leader :desc "Rotate frame"                 "w r" (cmd!
                                                     (if (t/prefix-arg-universal?)
                                                         (rotate-frame-anticlockwise)
                                                       (rotate-frame-clockwise)))

 :leader :desc "Projectile dired"    "p d" #'t/projectile-dired
 :leader :desc "Projectile magit"    "p g" #'t/projectile-magit-status
 :leader :desc "Projectile pulls"    "p P" #'t/projectile-visit-git-link-pulls

 :leader :desc "Scratch buffer"      "b s" #'doom/open-scratch-buffer

 :leader :desc "Previous occurrence" "h p" #'highlight-symbol-prev
 :leader :desc "Previous occurrence" "h N" #'highlight-symbol-prev
 :leader :desc "Next occurrence"     "h n" #'highlight-symbol-next)

Hide the last frame on os x instead of nuking it

(map! :leader "q f" 't/delete-frame-or-hide-last-remaining-frame)

That’s irritating. Prevent drag-stuff-mode from messing things up

(map!
 :after drag-stuff-mode
 :map drag-stuff-mode-map
 "<M-up>"    #'drag-stuff-up ;; messes up org mode
 "<M-down>"  #'drag-stuff-down ;; messes up org mode
 ;; :ni "<M-left>"  #'evil-backward-word-begin
 ;; :ni "<M-right>" #'evil-forward-word-begin
 )

Popup bindings on a norwegian keyboard

(map! :g "C-*"   #'+popup/raise
      :g "C-x p" #'+popup/other
      :leader "ø" #'+popup/toggle
      :map org-mode-map
      :g "C-*"   #'+popup/raise
      :g "C-ø"   #'+popup/toggle)

Gnus

(set-popup-rule! "^*Summary" :side 'bottom :size 0.5)
(set-popup-rule! "^*Article" :side 'bottom :size 0.5)
(setq gnus-select-method '(nntp "news.gmane.io")) ; A A

Help

One help shortcut, everywhere.

(map! :leader :n "h h" #'helpful-at-point)

Keep them on the side for some more room.

(set-popup-rule! "^*info" :side 'right :width 82)
(set-popup-rule! "^*help" :side 'right :width 82)
(set-popup-rule! "^*eglot-help" :side 'right :width 82)
(set-popup-rule! "^*cider-doc" :side 'right :width 82)

Motions

Make helpful buffers more navigable by removing doom popup’s dedication. This makes q fall back to the previous help buffer after a help link click that made you navigate to the next help topic.

(advice-add
 #'push-button
 :after (defun t/keep-help-buffers-around (&optional arg)
          (set-window-dedicated-p (selected-window) nil)
          (set-window-parameter (selected-window) 'no-delete-other-windows nil)))

Info mode

(after! info
  (map!
   :map Info-mode-map
   "M-n" #'forward-paragraph
   "M-p" #'backward-paragraph))

Motions

Motion keys for info mode.

(after! evil
  (after! info
    (evil-define-key 'normal Info-mode-map (kbd "H") 'Info-history-back)
    (evil-define-key 'normal Info-mode-map (kbd "L") 'Info-history-forward)
    (unbind-key (kbd "h") 'Info-mode-map)
    (unbind-key (kbd "l") 'Info-mode-map)))

Org

Org settings

(after! org

  (add-hook! 'org-mode-hook 'evil-cleverparens-mode)

  (defun t/open-prev-heading ()
    (interactive)
    (let ((was-narrowed (buffer-narrowed-p)))
      (when was-narrowed (widen))
      (when (org-at-heading-p)
        (outline-hide-sublevels (org-outline-level)))
      (org-previous-visible-heading 1)
      (outline-show-subtree)
      (when was-narrowed (org-narrow-to-subtree))
      (recenter-top-bottom 0)
      (progn ;; hack to make eldoc pop up
        (evil-previous-line)
        (evil-next-line)
        (evil-forward-word-begin))))


  (defun t/open-next-heading ()
    (interactive)
    (let ((was-narrowed (buffer-narrowed-p)))
      (when was-narrowed (widen))
      (when (org-at-heading-p)
        (outline-hide-sublevels (org-outline-level)))
      (org-next-visible-heading 1)
      (outline-show-subtree)
      (eldoc-print-current-symbol-info)
      (when was-narrowed (org-narrow-to-subtree))
      (recenter-top-bottom 0)
      (progn ;; hack to make eldoc pop up
        (evil-previous-line)
        (evil-next-line)
        (evil-forward-word-begin))))

  ;; like in normal org, not like in doom
  (map! :after evil-org
        :map evil-org-mode-map
        :ni "C-<return>" #'org-insert-heading-respect-content

        ;; bring back deleting characters from insert in org mode
        :i "C-d" nil

        :map org-mode-map
        :ni "C-c C-p" #'t/open-prev-heading
        :ni "C-c C-n" #'t/open-next-heading)

  ;; Include gpg files in org agenda
  (unless (string-match-p "\\.gpg" org-agenda-file-regexp)
    (setq org-agenda-file-regexp
          (replace-regexp-in-string "\\\\\\.org" "\\\\.org\\\\(\\\\.gpg\\\\)?"
                                    org-agenda-file-regexp)))

  (defun t/org-capture-chrome-link-template (&optional &rest args)
    "Capture current frontmost tab url from chrome."
    (concat "* TODO %? :url:\n\n" (t/grab-chrome-url)))

  (defun t/org-capture-link-template (&optional &rest args)
    "Capture url."
    (concat "* TODO %? %^G\n\nLink:\n - "
            (cond
             ((equal major-mode 'mu4e-view-mode) (concat "mu4e:msgid:" (plist-get (mu4e-message-at-point) :message-id)))
             ((equal major-mode 'mu4e-headers-mode) (concat "mu4e:msgid:" (plist-get (mu4e-message-at-point) :message-id)))
             ((equal major-mode 'elfeed-show-mode) (elfeed-entry-link elfeed-show-entry))
             ((equal major-mode 'elfeed-search-mode) (s-join "\n - " (cl-loop for feed in (elfeed-search-selected)
                                                                              collect (elfeed-entry-link feed))))
             ((equal major-mode 'eww-mode) (concat "%a"))
             ((equal major-mode 'org-mode) (concat "%a"))
             (t (get-text-property (point) 'shr-url)))))

  (setq org-tags-column -60
        org-hide-emphasis-markers t  ; hide symbols like ~ and / when wrapped around text
        org-support-shift-select t   ; shift can be used to mark multiple lines
        org-special-ctrl-k t         ; don't clear tags, etc
        org-special-ctrl-a/e t       ; don't move past ellipsis on c-e
        org-id-link-to-org-use-id t  ; create link if it doesnt exist, or when org-capture -ing (needs %a in template)
        org-attach-directory (t/user-dropbox-folder "/org/attachments")
        org-attach-id-to-path-function-list '(org-attach-id-ts-folder-format ;; saner attachment folder structure
                                              org-attach-id-uuid-folder-format)
        org-goto-interface 'outline-path-completion ;; more useful c-c c-j
        org-id-method 'ts
        org-agenda-skip-scheduled-if-done t
        org-default-notes-file (t/user-dropbox-folder "/org/home.org.gpg")
        org-log-done 'time           ; log when todos are completed
        org-log-redeadline 'time     ; log when deadline changes
        org-log-reschedule 'time     ; log when schedule changes
        org-reverse-note-order t     ; newest notes first
        org-return-follows-link t    ; go to http links in browser
        org-todo-keywords '((sequence "TODO(t)" "STARTED(s)" "NEXT(n)" "|" "DONE(d)" "CANCELLED(c)"))))

Show images, like webp

Use os support if it exists.

(setq image-use-external-converter t
      org-image-actual-width (list (float 0.5) (float 0.5)))

Variable pitch mode

(add-hook! 'org-mode-hook (defun t/variable-pitch-mode-some-buffers ()
                            (interactive)
                            (let ((bn (buffer-name)))
                              (when (or (s-ends-with? "posts.org" bn)
                                        (s-equals? "*ChatGPT*" bn))
                                (olivetti-mode 1)
                                (t/variable-pitch-mode 1)))))
(add-hook 'org-ai-mode-hook (defun t/org-ai-mode-hook ()
                              (interactive)
                              (advice-add
                               'org-ctrl-c-ctrl-c
                               :after
                               (defun t/org-ai-ctrl-c (&optional &rest any)
                                 (when (s-equals? "*ChatGPT*" (buffer-name))
                                   (end-of-buffer))))))

Async source code blocks

Make it possible to use the header argument :async true for async execution of begin_src code blocks.

(after! org
  (require 'ob-async))

Agenda

Custom commands

Org agenda customizations

(defun t/org-agenda-todo-type (name)
  `((org-agenda-remove-tags t)
    (org-agenda-sorting-strategy '(tag-up priority-down))
    (org-agenda-todo-keyword-format "")
    (org-agenda-overriding-header ,name)))

(defun t/org-agenda-day (tags)
  (list tags `((org-agenda-span 'day)
               (org-agenda-tag-filter-preset ,tags))))


(defun t/org-agenda-pri (header tags)
  (list tags `((org-agenda-overriding-header ,header)
               (org-agenda-skip-function '(or (org-agenda-skip-entry-if 'todo 'done)
                                              (and (org-agenda-skip-entry-if 'notregexp "\\[#A\\]")
                                                   (org-agenda-skip-entry-if 'notregexp "\\[#B\\]")
                                                   (org-agenda-skip-entry-if 'notregexp "\\[#C\\]")))))))

(defun t/org-agenda-not-pri (header tags skip)
  (list tags `((org-agenda-overriding-header ,header)
               (org-agenda-skip-function '(or (org-agenda-skip-entry-if 'regexp "\\[#A\\]")
                                              (org-agenda-skip-entry-if 'regexp "\\[#B\\]")
                                              (org-agenda-skip-entry-if 'regexp "\\[#C\\]")
                                              (org-agenda-skip-if nil (quote ,skip)))))))

(defun t/org-agenda-todos (header tags)
  (t/org-agenda-not-pri header tags '(scheduled deadline)))

(defun t/org-agenda-todos-scheduled (header tags)
  (t/org-agenda-not-pri header tags '(notscheduled deadline)))

(defun t/org-day-summary (&rest tags)
  `((agenda    ,@(t/org-agenda-day (string-join tags "|")))
    (tags      ,@(t/org-agenda-pri "Pri" (string-join tags "|")))
    (tags-todo ,@(t/org-agenda-todos "Todo" (string-join tags "|")))
    (tags-todo ,@(t/org-agenda-todos-scheduled "Scheduled todo" (string-join tags "|")))))

(defun t/org-agenda-read ()
  `(tags-todo "book|read|pocket" ((org-agenda-overriding-header "Read"))))

(defun t/org-done-today (tag)
  `(tags ,(format "%s+CLOSED>=\"<today>\"" tag) ((org-agenda-overriding-header "\nCompleted today\n"))))

;; and some custom agenda shortcuts using them
(setq org-agenda-custom-commands
      `(("n" "Agenda and all TODOs" ((agenda "") (alltodo "")))
        ("m" tags-todo "serie|film")
        ("e" tags-todo "emacs")
        ("r" ,@(t/org-agenda-read))
        ("v" tags-todo "video")
        ("T" alltodo)
        ("C" todo "DONE" ,(t/org-agenda-todo-type "DONE"))
        ("t" todo "TODO" ,(t/org-agenda-todo-type "TODO"))
        ("b" todo "STARTED" ,(t/org-agenda-todo-type "STARTED"))
        ("c" todo "CANCELLED" ,(t/org-agenda-todo-type "CANCELLED"))
        ("w" "work" ,(append (t/org-day-summary "+bekk" "+da")
                             `((tags "+someday+da")
                               (tags "+someday+bekk")
                               ,(t/org-done-today "+work"))))
        ("h" "home" ,(append (t/org-day-summary "+home-emacs-someday")
                             `(,(t/org-agenda-read)
                               (tags-todo "+someday-work" ((org-agenda-overriding-header "Someday")))
                               ,(t/org-done-today "+home"))))))

Clock

(defun t/org-clock-start (&optional &rest args)
  (interactive)
  (when (not (featurep 'org-pomodoro))
    (require 'org-pomodoro))
  (org-todo "STARTED"))
(defun t/org-clock-stop (&optional &rest args)
  (interactive)
  (when (not (featurep 'org-pomodoro))
    (require 'org-pomodoro))
  (when (not (org-pomodoro-active-p))
    (org-clock-jump-to-current-clock)
    (org-todo)))
(advice-remove 'org-clock-in 't/org-clock-start)
(advice-remove 'org-clock-out 't/org-clock-stop)
(advice-add 'org-clock-in :after 't/org-clock-start)
(advice-add 'org-clock-out :after 't/org-clock-stop)

Alerts

Setup alert.el to notify also on macos.

(setq alert-default-style (if is-mac 'osx-notifier 'libnotify))

Alert a 5 minutes before schedules or deadlines, keep it going for 10. Capture the first time string as the date like suggested in the readme.

(use-package! org-alert
  :init
  (setq org-alert-interval (* 5 60)
        org-alert-notify-cutoff 5
        org-alert-notify-after-event-cutoff 5
        org-alert-time-match-string "\\(?:SCHEDULED\\|DEADLINE\\):.*?<.*?\\([0-9]\\{2\\}:[0-9]\\{2\\}\\).*>")
  :config
  (org-alert-enable))

Keybindings

Extensions of some of the Doom org mode map bindings.

Heading and item bindings

C-ret
new below, insert mode, same level
C-S-ret
new above, insert mode, same level
M-ret
new heading, normal mode, same level
M-S-ret
todo below, normal mode, same level
C-M-ret
heading below, normal mode, level down
SPC-m-h
heading from text
SPC-m-i
item from text

SPC g a seems more reasonable than SPC g G. Localleader in doom is bound to SPC m. This also enables searching across all agenda files using SPC g A.

(map! :map org-mode-map
      :localleader "g a" #'consult-org-agenda
      :localleader "g A" (cmd! (consult-org-heading t 'agenda-with-archives)))

Widen

(map!
 :map org-mode-map
 :localleader :desc "Widen" "s w" 'widen
 :localleader :desc "Narrow to subtree" "s n" 'org-narrow-to-subtree)

Save from agenda

(map! :after org-agenda
      :map (evil-org-agenda-mode-map org-super-agenda-header-map)
      :g "h" nil
      :g "j" nil
      :g "k" nil
      :g "l" nil
      :m "H" #'org-agenda-earlier
      :m "L" #'org-agenda-later
      :m "d" #'org-agenda-day-view
      :m "w" #'org-agenda-week-view
      :m "y" #'org-agenda-year-view
      :m "m" #'org-agenda-month-view
      "s-s" #'org-save-all-org-buffers)

Colors

(after! org
  (set-face-attribute 'org-todo nil :foreground "#94fFe4" :weight 'bold))

Make links appear

(use-package! org-appear
  :hook (org-mode . org-appear-mode)
  :config
  (setq org-appear-autoemphasis t
        org-appear-autosubmarkers t
        org-appear-autolinks nil)
  ;; for proper first-time setup, `org-appear--set-elements'
  ;; needs to be run after other hooks have acted.
  (run-at-time nil nil #'org-appear--set-elements))

Org links

Make org handle links load links that start with

  • eww:
  • eshell
  • man:
  • vterm:
(add-hook! 'org-mode-hook
  (defun t/load-org-links ()
    (interactive)
    (require 'ol)
    (require 'ol-eshell)
    (require 'ol-man)
    (require 'ol-eww)
    (defun t/org-vterm-open (url _)
      "Open URL with vterm in the current buffer."
      (let ((current-prefix-arg 1))
        (call-interactively '+vterm/toggle)
        (term-send-raw-string (concat url "\C-m"))))
    (org-link-set-parameters "vterm" :follow 't/org-vterm-open)))

Refile

Save org mode buffers after refile.

(defadvice org-refile (after t/after-org-refile activate)
  (org-save-all-org-buffers))

Tables

(after! evil
  (when (boundp 'org-evil-table-mode-map)
    (map!
     :map org-evil-table-mode-map
     "M-S-<left>" 'org-table-delete-column
     "M-S-<right>" 'org-table-insert-column)))

Hugo

Allow ox-hugo to copy webp

(after! ox-hugo
  (add-to-list 'org-hugo-external-file-extensions-allowed-for-copying "webp"))

Capture template: Post

(after! org
  (with-eval-after-load 'org-capture
    (defun org-hugo-new-subtree-post-capture-template ()
      "Returns `org-capture' template string for new Hugo post.
See `org-capture-templates' for more information.
https://ox-hugo.scripter.co/doc/org-capture-setup/"
      (let* ((title (read-from-minibuffer "Post Title: "))
             (fname (org-hugo-slug title)))
        (mapconcat #'identity
                   `(,(concat "* TODO " title)
                     ":PROPERTIES:"
                     ,(concat ":EXPORT_FILE_NAME: " fname)
                     ":END:" "%?\n")
                   "\n")))))

Structure templates

Remove the s mapping for source code blocks.

(after! org
  (setq org-structure-template-alist (remove '("s" "src") org-structure-template-alist)))

Replace it with ss (its faster than the default ~s ~) so we can add some more along side it.

(after! org
  (add-to-list 'org-structure-template-alist (cons "ss" "src"))
  (add-to-list 'org-structure-template-alist (cons "se" "src emacs-lisp"))
  (add-to-list 'org-structure-template-alist (cons "sp" "src python"))
  (add-to-list 'org-structure-template-alist (cons "sn" "src nix"))
  (add-to-list 'org-structure-template-alist (cons "sj" "src javascript"))
  (add-to-list 'org-structure-template-alist (cons "sh" "src sh"))
  (add-to-list 'org-structure-template-alist (cons "aI" "ai :image :size 512x512"))
  (add-to-list 'org-structure-template-alist (cons "ai" "ai"))
  (add-to-list 'org-structure-template-alist (cons "d" "description")))

If you need to remove one, do this

(comment
 (setq org-structure-template-alist (assoc-delete-all "sh" org-structure-template-alist)))

Don’t popupize the org code block editor with doom’s popup framework, so it opens split wherever it fits like it is by default.

(after! org
  (set-popup-rule! "^*Org Src" :ignore t))

Capture templates

(after! org

  (setq org-capture-templates
        `(("t" "Task" entry (file+olp org-default-notes-file "tasks") "* TODO %? \n\n%i\n\n" :prepend t :empty-lines-after 1)
          ("d" "Da" entry (file+olp ,(t/user-dropbox-folder "org/da.org.gpg") "Tasks") "* TODO %? \n\n%i" :prepend t :empty-lines-after 1)
          ("b" "Bekk" entry (file+olp ,(t/user-dropbox-folder "org/bekk.org.gpg") "Tasks") "* TODO %? \n\n%i" :prepend t :empty-lines-after 1)
          ("f" "File/item (or elfeed)" entry (file+olp org-default-notes-file "Tasks") "* TODO %? %^G\n\n%i%a\n\n" :prepend t :empty-lines-after 1)
          ("l" "Link (eww, mu4e, etc)" entry (file+olp org-default-notes-file "Tasks") (function t/org-capture-link-template) :prepend t :empty-lines-after 1)
          ("c" "Chrome location" entry (file+olp org-default-notes-file "Tasks") (function t/org-capture-chrome-link-template) :prepend t :empty-lines-after 1)
          ("p" "Post" entry (file+olp "~/Code/posts/content-org/blog.org" "Drafts") (function org-hugo-new-subtree-post-capture-template)))))

Text Objects

evil-org-outer-subtree

(after! evil
  (evil-define-text-object evil-org-outer-subtree (count &optional beg end type)
    "An Org subtree.  Uses code from `org-mark-subtree`"
    :type line
    (save-excursion
      ;; get to the top of the tree
      (org-with-limited-levels
       (cond ((org-at-heading-p) (beginning-of-line))
             ((org-before-first-heading-p) (user-error "Not in a subtree"))
             (t (outline-previous-visible-heading 1))))

      (cl-decf count)
      (when count (while (and (> count 0) (org-up-heading-safe)) (cl-decf count)))

      ;; extract the beginning and end of the tree
      (let ((element (org-element-at-point)))
        (list (org-element-property :end element)
              (org-element-property :begin element))))))

evil-org-inner-subtree

(after! evil
  (evil-define-text-object evil-org-inner-subtree (count &optional beg end type)
    "An Org subtree, minus its header and concluding line break.  Uses code from `org-mark-subtree`"
    :type line
    (save-excursion
      ;; get to the top of the tree
      (org-with-limited-levels
       (cond ((org-at-heading-p) (beginning-of-line))
             ((org-before-first-heading-p) (user-error "Not in a subtree"))
             (t (outline-previous-visible-heading 1))))

      (cl-decf count)
      (when count (while (and (> count 0) (org-up-heading-safe)) (cl-decf count)))

      ;; extract the beginning and end of the tree
      (let* ((element (org-element-at-point))
             (begin (save-excursion
                      (goto-char (org-element-property :begin element))
                      (next-line)
                      (point)))
             (end (save-excursion
                    (goto-char (org-element-property :end element))
                    (backward-char 1)
                    (point))))
        (list end begin)))))

evil-org-outer-item

(after! evil
  (evil-define-text-object evil-org-outer-item (count &optional beg end type)
    :type line
    (let* ((struct (org-list-struct))
           (begin (org-list-get-item-begin))
           (end (org-list-get-item-end (point-at-bol) struct)))
      (if (or (not begin) (not end))
          nil
        (list begin end)))))

evil-org-inner-item

(after! evil
  (evil-define-text-object evil-org-inner-item (count &optional beg end type)
    (let* ((struct (org-list-struct))
           (begin (progn (goto-char (org-list-get-item-begin))
                         (forward-char 2)
                         (point)))
           (end (org-list-get-item-end-before-blank (point-at-bol) struct)))
      (if (or (not begin) (not end))
          nil
        (list begin end)))))

Bind them

(define-key evil-outer-text-objects-map "h" 'evil-org-outer-subtree)
(define-key evil-inner-text-objects-map "h" 'evil-org-inner-subtree)
(define-key evil-outer-text-objects-map "*" 'evil-org-outer-subtree)
(define-key evil-inner-text-objects-map "*" 'evil-org-inner-subtree)
(define-key evil-outer-text-objects-map "i" 'evil-org-outer-item)
(define-key evil-inner-text-objects-map "i" 'evil-org-inner-item)
(define-key evil-outer-text-objects-map "-" 'evil-org-outer-item)
(define-key evil-inner-text-objects-map "-" 'evil-org-inner-item)

Pomodoro

(after! org
  (setq org-pomodoro-format "%s"
        org-pomodoro-play-sounds nil
        org-pomodoro-length 25
        org-pomodoro-short-break-length 5
        org-pomodoro-long-break-length 10
        org-pomodoro-long-break-frequency 4))

Clock in like SPC m c i.

(map! :map org-mode-map
      :localleader
      (:prefix ("c" . "clock")
               "p" #'org-pomodoro))

Clock in like SPC m c i.

(map! :map org-mode-map
      :localleader
      (:prefix ("c" . "clock")
               "p" #'org-pomodoro))

OpenAI: GPT

Default openai language model.

(setq-default *t-gpt-models* "gpt-4o-mini")

org-ai: ChatGPT in org mode

(use-package! org-ai
  :hook (org-mode . org-ai-mode)
  :config
  (add-to-list 'org-ai-chat-models *t-gpt-models* t)
  (setq-default org-ai-default-chat-model *t-gpt-models*))

Popup on the side

(set-popup-rule! "^\\*ChatGPT" :size 0.45 :side 'right :quit 'other)

Shortcuts to pop open prompt with often used dialogs

(defun t/chatgpt-prompt (prompt)
  "Pop open an org mode buffer with the selection region and the given prompt
  prepended."
  (interactive)
  (t/chatgpt-buffer (region-beginning) (region-end) prompt))

(defun t/chatgpt-buffer (beg end &optional prompt)
  "Pop open an org mode buffer with the selection region and an optional prompt
  prepended."
  (interactive (list (and (mark t) (region-beginning))
                     (and (mark t) (region-end))))
  (let ((active-region (when (region-active-p)
                         (buffer-substring beg end)))
        (major-mode-name (symbol-name major-mode)))
    (with-current-buffer (pop-to-buffer "*ChatGPT*")
      (erase-buffer)
      (org-mode)
      (olivetti-mode)
      (insert
       "#+begin_ai"
       "\n"
       "[SYS]: You are a helpful, expert programming assistant, that lives inside of emacs. Respond in a way that is useful for a developer. Keep responses brief, don't extensively explain why something works, focus on how."
       "\n\n"
       "[ME]: \n#+end_ai")
      (move-end-of-line 0)
      (evil-insert 0)
      (save-excursion
        (when prompt (insert "#" prompt))
        (when active-region (insert "\n\n" active-region))))))

(defun t/chatgpt-send ()
  (interactive)
  (with-current-buffer (pop-to-buffer "*ChatGPT*")
    (call-interactively 'org-ctrl-c-ctrl-c)))

(map! :leader
      (:prefix
       ("o" . "open")
       (:prefix-map
        ("G" . "chatgpt")
        (:when t
          :desc "ask" "a" #'t/chatgpt-buffer
          :desc "fix" "f" (cmd! (t/chatgpt-prompt "Why doesn't this code work?"))
          :desc "explain" "e" (cmd! (t/chatgpt-prompt "What does this code do?") (t/chatgpt-send))
          :desc "gen tests" "t" (cmd! (t/chatgpt-prompt "Write a test for this code") (t/chatgpt-send))
          :desc "optimize" "o" (cmd! (t/chatgpt-prompt "Refactor this code for speed and tell me what you changed and why it's faster") (t/chatgpt-send))
          :desc "refactor" "r" (cmd! (t/chatgpt-prompt "Refactor this code and tell me what you changed and why it's better") (t/chatgpt-send))
          :desc "summarize" "s" (cmd! (t/chatgpt-prompt "Summarize this text:") (t/chatgpt-send))))))
(setq chatgpt-shell-model-version *t-gpt-models*
      chatgpt-shell-openai-key
      (lambda ()
        (auth-source-pick-first-password :host "api.openai.com")))

Reading

Mastodon

(after! mastodon
  (setq mastodon-instance-url "https://fosstodon.org"
        mastodon-active-user "@[email protected]")
  (set-popup-rule! "^*mastodon" :ignore t)
  (map! :map mastodon-mode-map
        :n "q" #'+workspace/kill
        :n "j" (cmd!
                (mastodon-tl--goto-next-item)
                (let ((current-prefix-arg '(4)))
                  (call-interactively 'recenter-top-bottom)))
        :n "k" (cmd!
                (mastodon-tl--goto-prev-item)
                (let ((current-prefix-arg '(4)))
                  (call-interactively 'recenter-top-bottom)))))

Fast

I never really got into this.

(defun t/spray-micro-state (&optional after)
  (t/micro-state-in-mode
   'spray-mode
   after
   "s" 'spray-slower
   "f" 'spray-faster
   "SPC" 'spray-start/stop
   "b" 'spray-backward-word
   "w" 'spray-forward-word
   "<left>" 'spray-backward-word
   "<right>" 'spray-forward-word))

(defun t/start-spray-micro-state (&optional on-exit)
  (interactive)
  (let ((map (make-sparse-keymap)))
    (bind-key (kbd "<wheel-right>") 'mwheel-scroll map)
    (bind-key (kbd "<wheel-left>") 'mwheel-scroll map)
    (bind-key (kbd "<wheel-up>") 'mwheel-scroll map)
    (bind-key (kbd "<wheel-down>") 'mwheel-scroll map)
    (bind-key "n" (lambda ()
                    (interactive)
                    (condition-case nil
                        (scroll-up-command)
                      (error
                       (cond
                        ((eq major-mode 'elfeed-show-mode) (elfeed-show-next))
                        ((eq major-mode 'mu4e-view-mode) (mu4e-view-headers-next)))))) map)
    (bind-key "p" (lambda ()
                    (interactive)
                    (condition-case nil
                        (scroll-down-command)
                      (error
                       (cond
                        ((eq major-mode 'elfeed-show-mode) (elfeed-show-prev))
                        ((eq major-mode 'mu4e-view-mode) (mu4e-view-headers-prev)))))) map)
    (bind-key "s" (cmd!
                   (when (eq major-mode 'elfeed-show-mode)
                     (let ((shr-inhibit-images t)) (elfeed-show-refresh)))
                   (funcall (t/spray-micro-state))) map)
    (bind-key "S" (cmd! (call-interactively 'ellama-summarize)) map)
    (bind-key "r" (cmd! (call-interactively 'eww-readable)) map)
    (bind-key "i" (cmd!
                   (setq shr-inhibit-images (not shr-inhibit-images))
                   (when (eq major-mode 'elfeed-show-mode)
                     (elfeed-show-refresh))
                   (when (eq major-mode 'eww-mode)
                     (call-interactively 't/eww-toggle-images))) map)
    (bind-key "l" (cmd! (call-interactively 'link-hint-open-link)) map)
    (bind-key "v" (cmd! (call-interactively 't/variable-pitch-mode)) map)
    (bind-key "o" (cmd! (call-interactively 'olivetti-mode)) map)
    (set-temporary-overlay-map map t on-exit))
  (when (not (minibuffer-window-active-p (selected-window)))
    (message "(n)ext page, (p)rev page, (i)mages, (r)eadability, (s)pray mode, (S)ummarize, (o)livetti, (v)ariable pitch")))

(map! :leader :desc "Toggle spray" "t s" (t/spray-micro-state))

(after! spray
  (setq spray-wpm 720
        spray-height nil)
  (add-hook 'spray-mode-hook #'t/spray-mode-hook)
  (defun t/spray-mode-hook ()
    (setq-local spray-margin-top (truncate (/ (window-height) 2.5)))
    (setq-local spray-margin-left (truncate (/ (window-width) 3.5)))
    (set-face-foreground 'spray-accent-face
                         (face-foreground 'font-lock-keyword-face))))

Eww

An elisp web browser.

Make it emacs default

This makes RET on a url open eww. You can still open an external browser with SPC u RET. Some urls, like github, open in the external browser.

(setq blacklisted-eww-url-parts '("localhost"
                                  "slack.com"
                                  "github.com"
                                  "openai.com"
                                  "twitter.com"
                                  "googleusercontent.com")
      browse-url-browser-function
      (lambda (url &optional _new-window)
        (let* ((parsed-url (url-generic-parse-url url))
               (host (url-host parsed-url)))
          (message "browse url: %s" parsed-url)
          (cond
           ((-any-p (lambda (url-part) (and host (s-contains? url-part host))) blacklisted-eww-url-parts)
            (browse-url-default-browser url))
           ((and host (or (s-contains-p "youtube.com" host) (s-contains-p "youtu.be" host)))
            (elfeed-tube-fetch url)
            (run-at-time "0 sec" nil 't/start-spray-micro-state))
           (t (eww-browse-url url))))))

Lookup

Make SPC s o open in eww first, then use & to go to the default browser if needed.

(setq +lookup-open-url-fn #'eww)

Popup size

(after! evil
  ;; the original way
  ;;(setf (alist-get 'size (display-buffer-assq-regexp "*eww*" display-buffer-alist nil)) 0.6)
  ;; the doom way
  (set-popup-rule! "^\\*eww*" :side 'right :size 0.7 :vslot 10))

Readability

Enter readable mode automatically, normally available from pressing R in eww mode.

(add-hook 'eww-after-render-hook (cmd! (call-interactively 'eww-readable)))
(add-hook 'eww-after-render-hook 'olivetti-mode)
(add-hook 'eww-after-render-hook 't/variable-pitch-mode)
(add-hook 'eww-after-render-hook 't/start-spray-micro-state)

Eww functions that directly enter the eww readability mode after loading a given url

(defun t/eww-readable-after-render (status url buffer fn)
  (eww-render status url nil buffer)
  (switch-to-buffer buffer)
  (eww-readable)
  (let ((content (buffer-substring-no-properties (point-min) (point-max))))
    (read-only-mode 0)
    (erase-buffer)
    (insert content)
    (beginning-of-buffer)
    (when fn (funcall fn))))

(defun t/eww-readable (url &optional fn)
  (interactive "sEnter URL: ")
  (let ((buffer (get-buffer-create "*eww*")))
    (with-current-buffer buffer
      (autoload 'eww-setup-buffer "eww")
      (eww-setup-buffer)
      (url-retrieve url 't/eww-readable-after-render (list url buffer fn)))))

Images and wrap long lines

(after! shr
  ;; don't truncate lines in
  (defun shr-fill-text (text) text)
  (defun shr-fill-lines (start end) nil)
  (defun shr-fill-line () nil)

  ;; not too large images
  (setq shr-use-fonts nil
        shr-max-image-proportion 0.6
        shr-ignore-cache t))

Hook and keybindings

Some useful eww keybindings

(after! eww
  (defun t/eww-hook ()
    (map!
     :map evil-normal-state-local-map
     "q" 'quit-window
     "S-TAB" 'shr-previous-link
     "TAB" 'shr-next-link
     "R" 'eww-readable
     "M-p" 'backward-paragraph
     "M-n" 'forward-paragraph
     "s-l" 'eww
     "s" (t/spray-micro-state))))
(add-hook 'eww-mode-hook #'t/eww-hook)

Hackernews

(use-package! hnreader
  :commands (hnreader-news)
  :config
  (set-popup-rule! "^*HN" :ignore t))

Reddit

(use-package! reddigg
  :commands (reddigg-view-main)
  :config
  (progn
    (set-popup-rule! "^*reddigg" :ignore t)
    (setq reddigg-subs '(doomemacs emacs minilab homelab homeserver orgmode sffpc archlinux))))

Nrk.no

A custom function to fetch a clean view of the current news from nrk.no

(defun t/clean-nrk-buffer ()
  (flush-lines "^$")
  ;; clean up lines beginning with dates, e.g. 20. sept...
  (beginning-of-buffer)
  (flush-lines "^[0-9][0-9]\.")

  ;; clean up lines beginning with -
  (beginning-of-buffer)
  (t/cleanup-buffer-whitespace-and-indent)
  (while (re-search-forward "*" nil t)
    ;; change * to -
    (replace-match "\n-")
    ;; highlight the line
    (add-text-properties (point-at-bol) (point-at-eol) '(face outline-4)))

  (beginning-of-buffer)

  ;; kill more lines with dates
  (while (re-search-forward "^[0-9][0-9]\." nil t)
    (when (string-match-p "^[0-9][0-9]\. [jfmasond]" (thing-at-point 'line))
      (beginning-of-line) (kill-line) (forward-line) (join-line)))

  ;; remove leading line
  (beginning-of-buffer)
  (kill-line)

  ;;(darkroom-mode)
  (read-only-mode)
  (funcall (t/micro-state (t/prefix-arg-universal?)
                          "n" (cmd! nil
                                    (evil-search "^-" t t)
                                    (evil-ex-nohighlight)
                                    (recenter nil))
                          "p" (cmd! nil
                                    (evil-search "^-" nil t)
                                    (evil-ex-nohighlight)
                                    (recenter nil))
                          "s" (t/spray-micro-state))))

Languages

Eglot language server

(after! eglot
  (setq eglot-connect-timeout (* 60 20)
        ;; don't block while waiting, defaults to 3
        eglot-sync-connect nil))

Nix

(after! eglot
  ;; (setq eglot-server-programs
  ;;       (cl-remove-if (lambda (c) (eq (car c) 'nix-mode)) eglot-server-programs))
  (add-to-list 'eglot-server-programs '(nix-mode . ("nil"))))
(after! nix-mode
  (add-hook! 'nix-mode-hook 'eglot-ensure))

Markdown

Move around like in org, collapsing what is moved away from, expanding what is moved to.

(map! :map markdown-mode-map
      "C-c C-p" (cmd!
                 (outline-show-all)
                 (outline-hide-body)
                 (markdown-outline-previous)
                 (outline-show-entry))
      "C-c C-n" (cmd!
                 (outline-show-all)
                 (outline-hide-body)
                 (markdown-outline-next)
                 (outline-show-entry)))

Pretty markdown

(after! markdown-mode
  (add-hook! 'markdown-mode-hook (defun t/markdown-toggle-pretty ()
                                   (interactive)
                                   ;; TODO
                                   ;;(markdown-toggle-url-hiding)
                                   ;;(markdown-toggle-markup-hiding)
                                   ;;(markdown-toggle-inline-images)
                                   )))

Bash/Sh

npm install -g bash-language-server

Clojure

Adapt cleverparens keys that clash with my M-[hjkl] bindings in ~/.skhdrc

(after! evil
  (map! :map evil-cleverparens-mode-map
        "C-M-h" 'evil-cp-beginning-of-defun
        "C-M-l" 'evil-cp-end-of-defun
        "C-M-k" 'evil-cp-drag-backward
        "C-M-j" 'evil-cp-drag-forward))
(after! clojure-mode
  (add-hook! '(clojure-mode-hook
               clojurec-mode-hook
               clojurescript-mode-hook) 'evil-cleverparens-mode)
  (map! :map clojure-mode-map "DEL" #'sp-backward-delete-char))

Holding alt for moving between sexps feel right

(after! clojure-mode
  (map! :map clojure-mode-map
        :i "M-<right>" #'evil-cp-forward-sexp
        :i "M-<left>" #'evil-cp-backward-sexp)
  (map! :map clojurec-mode-map
        :i "M-<right>" #'evil-cp-forward-sexp
        :i "M-<left>" #'evil-cp-backward-sexp)
  (map! :map clojurescript-mode-map
        :i "M-<right>" #'evil-cp-forward-sexp
        :i "M-<left>" #'evil-cp-backward-sexp))

Doom removes cider auto completion, bring it back, by adding it to the front of completion-at-point-functions.

(after! clojure-mode
  (remove-hook! 'cider-mode-hook '+clojure--cider-disable-completion)
  (add-hook! 'clojure-mode-hook :append
    (defun t/enable-cider-autocomplete-again ()
      (interactive)
      (add-hook 'completion-at-point-functions #'cider-complete-at-point -99 t))))

Emacs lisp

;; TODO bind M-SPC in minibuffer-mode-map
(map! :map (minibuffer-mode-map emacs-lisp-mode-map)
      :localleader :desc "Eval and replace" "e R" #'t/eval-and-replace)
(after! evil
  (add-hook 'emacs-lisp-mode-hook #'evil-cleverparens-mode))

Show containing parens, when the cursor is inside theme.

(define-advice show-paren-function (:around (fn) fix)
  "Highlight enclosing parens."
  (cond ((looking-at-p "\\s(") (funcall fn))
        (t (save-excursion
             (ignore-errors (backward-up-list))
             (funcall fn)))))

Terraform

Highlight terraform plans in terraform-mode based on their file name.

(add-to-list 'auto-mode-alist (cons (concat "^" (t/user-file "Downloads/") "tf_plan_.*") 'terraform-mode))
(after! terraform-mode
  (defun t-tf-plan-hook ()
    (interactive)
    (flycheck-mode -1)
    (when (s-contains-p "tf_plan_" buffer-file-name)
      (beginning-of-buffer)
      (evil-search "^───" t t)
      (call-interactively 'evil-scroll-line-to-top)))
  (add-hook! 'terraform-mode-hook #'terraform-format-on-save-mode)
  (add-hook! 'terraform-mode-hook #'t-tf-plan-hook))

Search for terraform resources

(defun t/tf-grep ()
  (interactive)
  (+vertico/project-search
   nil
   "^\\(\\<resource\\>\\|\\<output\\>\\|\\<variable\\>\\|\\<module\\>\\|\\<variable\\>\\)#\\(\"[^\"]+\"\\)~ \\(\"[^\"]+\"\\)?~ {"
   ))

Eglot ls

case $(uname) in
    Darwin)
        brew install hashicorp/tap/terraform-ls
        ;;
    Linux)
        false
        ;;
esac
(after! eglot
  (add-to-list 'eglot-server-programs '((hcl-mode terraform-mode) . ("terraform-lsp"))))

Kotlin

This does not work well with eglot

(after! eglot
  ;;(-find (lambda (c) (eq (car c) 'kotlin-mode)) eglot-server-programs)
  (setq eglot-server-programs
        (cl-remove-if (lambda (c) (eq (car c) 'kotlin-mode)) eglot-server-programs)))

Eglot ls

wget https://github.com/fwcd/kotlin-language-server/releases/download/1.3.1/server.zip -O ~/bin/server.zip
cd ~/bin/
rm -rf server
unzip server.zip
ln -sf server/bin/kotlin-language-server .

Python

Eglot ls

Create a venv in venv.

python3 -m venv ~/.doom.d/test-files/venv
(add-to-list 'auto-mode-alist
             (cons (concat "^" (t/user-emacs-file "test-files/*"))
                   (defun t/activate-pyenv-test-files ()
                     (pyvenv-activate (t/user-emacs-file "test-files/venv")))))

Install a language server in the venv.

case $(uname) in
    Darwin)
        #pip install python-lsp-server
        pip install 'python-lsp-server[all]'
        ;;
    Linux)
        false
        ;;
esac

Need C-c C-s in org mode.

(after! pyenv-mode
  (map! :map 'pyenv-mode-map "C-c C-s" nil))

remark-mode

(use-package! remark-mode
  :commands remark-mode
  :init
  (set-popup-rule! "*remark browser*" :ttl nil))

Javascript/Typescript

Eglot typescript-language-server setup

Prerequisites

npm install -g typescript-language-server typescript

Remove what the doom (javascript :lsp) module sets up for web mode typescript

(setq auto-mode-alist (assoc-delete-all "\\.tsx?\\'" auto-mode-alist))
(setq auto-mode-alist (assoc-delete-all "\\.tsx\\'" auto-mode-alist))
(add-to-list 'auto-mode-alist '("\\.ts\\'" . typescript-ts-mode) t)
(add-to-list 'auto-mode-alist '("\\.tsx\\'" . tsx-ts-mode) t)

Get rid of tide, it keeps starting tsserver. I don’t want it.

(after! tide
  (when (featurep 'evil-collection-tide) (unload-feature 'evil-collection-tide))
  (when (featurep 'company-tide) (unload-feature 'company-tide))
  (when (featurep 'tide) (unload-feature 'tide)))

Remove what eglot-server-programs contains, that is the typescript-language-server setup without inlay hints

(after! eglot
  ;; get rid of the old configuration
  ;; remove configuration where the first item is js-mode in the nested list
  ;; could not find a simpler to remove the default configuration than this
  (setq eglot-server-programs
        (cl-remove-if (lambda (el) (and
                                    (listp (car el))
                                    (listp (caar el))
                                    (equal (caaar el) 'js-mode)))
                      eglot-server-programs))

  ;; insert the new configuration that sets options to include inlay hints
  ;; https://github.com/joaotavora/eglot/discussions/1266
  ;; https://www.reddit.com/r/emacs/comments/11bqzvk/comment/jg0hlm4
  (let ((inlay-opts '("typescript-language-server" "--stdio"
                      :initializationOptions
                      (:preferences
                       (:includeInlayParameterNameHints "all"
                        :includeInlayParameterNameHintsWhenArgumentMatchesName t
                        :includeInlayVariableTypeHintsWhenTypeMatchesName t
                        :includeInlayPropertyDeclarationTypeHints t
                        :includeInlayFunctionLikeReturnTypeHints t
                        :includeInlayFunctionParameterTypeHints t
                        :includeInlayEnumMemberValueHints t
                        :includeInlayVariableTypeHints t)))))
    (add-to-list
     'eglot-server-programs
     ;; stole this from the original eglot-server-programs
     `((;; this messes up json-mode which inherits javascript-mode
        ;;(js-mode :language-id "javascript")

        (js-ts-mode :language-id "javascript")
        (tsx-ts-mode :language-id "typescriptreact")
        (typescript-ts-mode :language-id "typescript")
        (typescript-mode :language-id "typescript"))
       .
       ,inlay-opts))))
(defun t/eglot-organize-imports ()
  (interactive)
  (if (derived-mode-p 'rjsx-mode
                      'typescript-ts-base-mode)
      (seq-do
       (lambda (kind)
         (interactive)
         (ignore-errors
           (eglot-code-actions (buffer-end 0)
                               (buffer-end 1) kind t)))
       ;; https://github.com/typescript-language-server/typescript-language-server#code-actions-on-save
       (list
        "source.addMissingImports.ts"
        "source.fixAll.ts"
        "source.removeUnused.ts"
        "source.addMissingImports.ts"
        "source.removeUnusedImports.ts"
        "source.sortImports.ts"
        "source.organizeImports.ts"
        ))
    (funcall-interactively #'eglot-code-action-organize-imports)))
(defun t-ts-mode-hook ()
  (add-hook 'before-save-hook #'t/eglot-organize-imports -100 t)
  (electric-indent-mode -1)
  (lsp!))

(add-hook! '(rjsx-mode-hook typescript-ts-mode-hook tsx-ts-mode-hook) #'t-ts-mode-hook)

Typescript repl with ts-node

Adapt what nodejs-repl does using ts-node.

npm install -g ts-node

You need a tsconfig.json for the following to work.

cat <<EOF > $DOOMDIR/test-files/tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "jsx": "react",
    "esModuleInterop": true
  }
}
EOF

Then make doom understand typescript, e.g. when you select something and go SPC o r.

(defun t/ts-node-repl ()
  (interactive)
  (pop-to-buffer
   (make-comint "ts-node repl" "ts-node")))
(set-repl-handler! '(typescript-ts-mode tsx-ts-mode) #'t/ts-node-repl)

Or when you eval something when the repl is open, e.g. g r a p (eval around paragraph). For multiline stuff type .editor into the repl before running the command, finish it with C-c C-d

(set-eval-handler! '(typescript-ts-mode tsx-ts-mode)
  '((:command     . "ts-node")
    (:exec        . "%c %s")
    (:description . "Run ts-node script")))

(eval +overlay) works in elisp, but is not that good for js/ts, so limit it to use the popup instead.

(setq +eval-popup-min-lines 0)

M-ret, like intellij

(map!
 :after rjsx-mode
 :map rjsx-mode-map :g "M-<return>" #'eglot-code-actions)
(map!
 :after typescript-ts-mode
 :map (typescript-ts-mode-map tsx-ts-mode-map)
 :g "M-<return>" #'eglot-code-actions)

Rust

Larger compilation window

(set-popup-rule! "^\\*rustic-compilation" :size 0.5 :side 'bottom)

Server mode

Emacs server setup.

e and et on the command line target the running emacs server instance, to quickly open a file or folder.

I also use this from Alfred, as a quick way of capturing from anywhere.

/etc/profiles/per-user/torgeir/bin/emacsclient -e '(progn (select-frame-set-input-focus (selected-frame)) (org-capture))'

Terminal

Vterm

This is paired with the bash function vterm_set_directory that updates the current working directory for emacs as the vterm path changes.

(setq vterm-shell "/usr/bin/env zsh")
(after! vterm
  ;; https://github.com/akermu/emacs-libvterm#how-can-i-get-the-directory-tracking-in-a-more-understandable-way
  ;; see dotfiles/source/functions
  (add-to-list
   'vterm-eval-cmds
   '("update-pwd" (lambda (path) (setq default-directory path))))

  (add-to-list
   'vterm-eval-cmds
   '("magit-diff" (lambda (path)
                    (let ((default-directory path))
                      (call-interactively' magit-diff)))))

  (add-to-list
   'vterm-eval-cmds
   '("magit-log" (lambda (path)
                   (let ((default-directory path))
                     (call-interactively' magit-log)))))

  (add-to-list
   'vterm-eval-cmds
   '("magit-status" (lambda (path)
                      (let ((default-directory path))
                        (call-interactively' magit-status))))))

Make pasting from consult-yank-from-kill-ring actually insert results in vterm.

(defun my/insert-for-yank-vterm-shim (orig-fun &rest args)
  (if (eq major-mode 'vterm-mode)
      (let ((inhibit-read-only t))
        (apply #'vterm-insert args))
    (apply orig-fun args)))
(advice-add #'+default/newline :around #'my/insert-for-yank-vterm-shim)
(advice-add #'insert-char-preview :around #'my/insert-for-yank-vterm-shim)
(advice-add #'insert-for-yank :around #'my/insert-for-yank-vterm-shim)

Keep evil insert mode cursor after shell commands have run.

(advice-add #'vterm--redraw :after (lambda (&rest args) (evil-refresh-cursor evil-state)))
(advice-add #'vterm--delayed-redraw :after (lambda (&rest args) (evil-refresh-cursor evil-state)))

Color black

The black in the terminal, e.g. when prefixing commands with a #, is a little to similar to my theme background. Brighten it slightly.

(after! vterm
  (set-face-attribute 'vterm-color-black nil :background "#595B6E" :foreground "#454758"))

This also fixes the color to be the one tmux uses with the catppuccin theme.

Keybindings

Some keybindings are so engrained I can’t live without them.

(after! vterm
  (map! :map vterm-mode-map
        ;; (vterm-send-key KEY &optional SHIFT META CTRL)

        ;; undo like in the terminal
        :i "C-_" (cmd! (vterm-send-key "_" t nil t))
        :i "M-<return>" (cmd! (vterm-send-key "<return>" nil t nil))
        :i "C-<return>" (cmd! (vterm-send-key "<return>" nil nil t))

        :i "M-<up>" (cmd! (vterm-send-key "<up>" nil t nil))
        :i "M-<right>" (cmd! (vterm-send-key "<right>" nil t nil))
        :i "M-<down>" (cmd! (vterm-send-key "<down>" nil t nil))
        :i "M-<left>" (cmd! (vterm-send-key "<left>" nil t nil))
        :i "M-d" (cmd! (vterm-send-key "d" nil t nil))
        :i "M-D" (cmd! (vterm-send-key "D" nil t nil))

        ;; send c-up c-down for dir stack zsh navigation
        :i "C-<up>" (cmd! (vterm-send-key "<up>" nil nil t))
        :i "C-<down>" (cmd! (vterm-send-key "<down>" nil nil t))
        :i "s-<up>" (cmd! (vterm-send-key "<up>" nil nil t))
        :i "s-<down>" (cmd! (vterm-send-key "<down>" nil nil t)))

  (map! :map vterm-mode-map
        :m "C-a" (cmd! (vterm-send-key "a" nil nil t))
        :m "M-<backspace>" (cmd! (vterm-send-key "w" nil nil t))
        :i "M-<backspace>" (cmd! (vterm-send-key "w" nil nil t))
        :i "C-h" (cmd! (vterm-send-key "h" nil nil t))
        ))

Remove C-l in normal mode, so that recenter-top-bottom works. Bring it back in insert mode.

(after! vterm
  (map! :map vterm-mode-map "C-l" nil)
  (map! :map vterm-mode-map :i "C-l" 'vterm-clear))

First esc sends escape, second exits to emacs normal mode

(after! vterm
  (comment
   ;; TODO remove
   (defun t/vterm-escape-second-time-around ()
     (interactive)
     (if (eq last-command 't/vterm-escape-second-time-around)
         (call-interactively 'evil-force-normal-state)
       (call-interactively 'vterm-send-escape)))
   (map! :map vterm-mode-map
         :i "<escape>" 't/vterm-escape-second-time-around))
  ;; this is good enough?
  (map! :map vterm-mode-map
        :i "M-<escape>" 'vterm-send-escape)
  (map! :map vterm-mode-map
        :i "M-:" 'eval-expression))

Allow vim-like copy from tmux to emacs kill ring

(after! vterm
  (setq vterm-enable-manipulate-selection-data-by-osc52 t))

Allow edit-command from insert mode

Use it with ^x^e

(after! vterm
  ;; ^xe edit-command-line in zshrc
  ;; ^x^e edit-command-line in zshrc
  (map! :map vterm-mode-map
        :i "C-x e" (cmd!
                    (vterm-send-key "x" nil nil t)
                    (vterm-send-key "e" nil nil nil))
        :i "C-x C-e" (cmd!
                      (vterm-send-key "x" nil nil t)
                      (vterm-send-key "e" nil nil t))))
Edit server custom keybinding to fix c-x c-e

Hack to make C-c C-c accept, and C-c C-k exit, when running ^x^e in a vterm terminal. Forces save upon accepting the content and cleans up the hanging “Waiting for Emacs…” when we return to vterm by sending C-l.

(after! sh-script
  (map! :map sh-mode-map :g "C-c C-c" nil)
  (add-hook! 'sh-mode-hook
    (defun t/sh-mode-server-hook ()
      (interactive)
      ;; when is visiting a window that belongs to an emacsclient
      (when server-clients
        (map! :map (evil-insert-state-local-map evil-normal-state-local-map)
              :g "C-c C-k" 'server-edit-abort
              :g "C-c C-c" (defun t/server-edit ()
                             (interactive)
                             (call-interactively 'save-buffer)
                             (server-edit)
                             (run-at-time "0 sec" nil (cmd! (term-send-raw-string "\C-l")))))))))

Terminal from everywhere with s-return

Make s-ret (super+enter) create a vterm terminal window inside emacs.

(map! :gn [s-return]
      (defun t/vterm-here ()
        (interactive)
        (if (eq major-mode 'dired-mode)
            (let* ((selected-dir (dired-get-marked-files t current-prefix-arg))
                   (selected-path (concat default-directory (car selected-dir)))
                   (default-directory
                    (if (file-directory-p selected-path)
                        selected-path
                      (file-name-directory selected-path))))
              (+vterm/here t))
          (+vterm/here t))))

Goes great with [[file:~/.config/dotfiles/skhdrc::cmd - return \[][these lines from ~/.skhdrc]], that make super+enter create a terminal from other apps.

Close comint buffers with c-d

(map! :map comint-mode-map
      :n "C-d" (cmd! (call-interactively 'evil-scroll-down))
      :i "C-d" #'t/volatile-kill-buffer-and-window)
(after! cider
  (map! :map cider-repl-mode-map
        :n "C-d" (cmd! (call-interactively 'evil-scroll-down))
        :i "C-d" #'t/volatile-kill-buffer-and-window))

Disable some modes in vterm

which-key messes with evil movement in vterm, e.g. when attemting to jump somewhere and perform an action on an evil text object, like yiw. This delays it long enough so you can finish your movement command before it kicks in, preventing it from interfering, only when in vterm-mode:

(after! which-key
  (defun t/delayed-which-key (_ _)
    "Suggested in https://github.com/justbur/emacs-which-key/issues/243"
    (cond
     ((eq major-mode 'vterm-mode) 2)
     (t nil)))
  (add-hook! 'which-key-delay-functions #'t/delayed-which-key))

(add-hook! 'vterm-mode-hook (defun t/vterm-mode-hook ()
                              (interactive)
                              (global-emojify-mode -1)
                              (eros-mode -1)))

Fix +default/search-buffer (consult-line) resetting cursor

By entering vterm-copy-mode before running the search vterm will be prevented from resetting the cursor, so the jump with consult-line can be allowed.

(defun t/around-vterm (fn)
  "Enter `vterm-copy-mode' before jumping with `consult-line' so that the cursor is not reset when the match is chosen in consult. Restore normal vterm mode by hitting `<return>' after."
  (interactive)
  (if (not (eq major-mode 'vterm-mode))
      (funcall fn)
    (progn
      (vterm-copy-mode)
      (funcall fn))))
(advice-remove '+default/search-buffer 't/around-vterm)
(advice-add '+default/search-buffer :around 't/around-vterm)

VC

Ediff

Sometimes you need both changes.

(after! ediff
  (defun t/bind-ediff-use-both ()
    (define-key ediff-mode-map "d" 't/ediff-use-both))
  (add-hook! 'ediff-keymap-setup-hook #'t/bind-ediff-use-both))

Magit and Forge

Useful magit keybindings:

S-SPC
preview commit
gj
next and preview
j
next

magit-log-arguments and the like are not ment to be used like a list you add args to, instead set options in the magit transient buffer by toggling them and saving it with c-x c-s.

(after! magit
  (setq git-commit-summary-max-length 72 ;; like github
        magit-display-buffer-function 'magit-display-buffer-same-window-except-diff-v1)

  (defun t/commit-truncate ()
    (visual-line-mode -1)
    (toggle-truncate-lines 1))
  (add-hook! '(magit-log-mode-hook magit-status-mode-hook) 't/commit-truncate)

  (defun t/commit-mode-hook ()
    (add-to-list 'whitespace-style 'trailing)
    (whitespace-mode 1)
    (t/commit-truncate))
  (add-hook! 'git-commit-mode-hook 't/commit-mode-hook)

  (set-popup-rule! "^magit:" :ignore t)
  (set-popup-rule! "^magit-revision" :side 'right :size 0.5))

Extend leader map with gn and gN, for navigating hunks, the g] and g[ bindings never made sense to me. And gca for amending.

(map!
 :after git-gutter
 :leader
 (:prefix-map
  ("g" . "git")
  (:when (modulep! :ui vc-gutter)
    :desc "Stage hunk"            "s" (cmd! (let ((git-gutter:ask-p nil))
                                              (git-gutter:stage-hunk)))
    :desc "Jump to next hunk"     "n" (cmd! (call-interactively 'git-gutter:next-hunk)
                                            (call-interactively 'evil-scroll-line-to-center))
    :desc "Jump to previous hunk" "N" (cmd! (call-interactively 'git-gutter:previous-hunk)
                                            (call-interactively 'evil-scroll-line-to-center)))))
(map!
 :after magit
 :leader
 (:prefix-map
  ("g" . "git")
  (:when (modulep! :tools magit)
    :desc "Diff dwim"             "d" #'magit-diff-dwim
    :desc "Ediff dwim"            "e" #'magit-ediff-dwim
    :desc "Visit pulls"           "p" #'t/visit-git-link-pulls
    :desc "Push"                  "P" #'magit-push
    (:prefix ("c" . "create")
     :desc "Ammend"               "a" #'magit-commit-amend
     :desc "Instant fixup"        "F" #'magit-commit-instant-fixup))))

I have been trying to get used to magit in evil mode for a while now. But the magit-process-buffer keybinding is crazy on a norwegian keyboard, so this brings back the binding from the emacs mode magit.

(map!
 :map magit-status-mode-map
 :desc "Show process buffer" :n "$" #'magit-process-buffer)

Colorz.

(after! magit
  (set-face-attribute 'magit-diff-hunk-heading nil :background "#513d5b" :foreground "#07010E")
  (set-face-attribute 'magit-diff-hunk-heading-highlight nil :background "#ED60BA" :foreground "#01010E" :weight 'bold)
  (set-face-attribute 'magit-diff-revision-summary nil :inherit 'magit-diff-hunk-heading :foreground "#ED60BA"))

Browse-at-remote opens github

Make the hostnames personal and work from ~/.ssh/config resolve to github.com, so that commands like SPC g o o opens github.

(after! browse-at-remote
  (add-to-list 'browse-at-remote-remote-type-regexps '(:host "^personal$" :type "github" :actual-host "github.com"))
  (add-to-list 'browse-at-remote-remote-type-regexps '(:host "^work$" :type "github" :actual-host "github.com")))

More colors in diff

Improved diffs with magit-delta-mode. Show the themes with delta --show-syntax-themes --dark | grep -i theme.

(setq magit-delta-default-dark-theme "DarkNeon")

Conventional commits

(defun magit-log-propertize-keywords-conventional-commits (_rev msg)
  (let ((boundary 0))
    (when (string-match "^\\(?:squash\\|fixup\\)! " msg boundary)
      (setq boundary (match-end 0))
      (magit--put-face (match-beginning 0) (1- boundary)
                       'magit-keyword-squash msg))
    (when magit-log-highlight-keywords
      ;; Case [...]
      (while (string-match "\\[[^[]*?]" msg boundary)
        (setq boundary (match-end 0))
        (magit--put-face (match-beginning 0) boundary
                         'magit-keyword msg))
      ;; Conventional commits
      (while (string-match "^\\(?:feat\\|fix\\|chore\\|docs\\|style\\|refactor\\|perf\\|test\\)\\(?:\\(?:[(].*[)]\\)\\|\\(?:!\\)\\)?:" msg boundary)
        (setq boundary (match-end 0))
        (magit--put-face (match-beginning 0) boundary
                         'magit-keyword msg))))
  msg)

(advice-add #'magit-log-propertize-keywords :override #'magit-log-propertize-keywords-conventional-commits)

Applications

Artist

(defun t/artist-mode ()
  (interactive)
  (if (and (boundp 'artist-mode)
           artist-mode)
      (progn
        (artist-mode-off)
        (evil-normal-state))
    (progn
      (switch-to-buffer "*scratch*")
      (evil-insert-state)
      (artist-mode t))))

(after! artist
  (add-hook! 'artist-mode-hook
    (defun t/artist-mode-hook ()
      (map!
       :map evil-insert-state-local-map "q" 'artist-mode-off
       :map evil-normal-state-local-map "q" 'artist-mode-off))))

(map!
 :leader
 (:prefix-map
  ("z" . "misc")
  (:prefix
   ("z" . "artist")
   (:when t
     :desc "Enable"          "t" 't/artist-mode
     :desc "Draw: pen"       "p" 'artist-select-op-pen-line
     :desc "Draw: line"      "l" 'artist-select-op-line
     :desc "Draw: rectangle" "r" 'artist-select-op-rectangle
     :desc "Draw: circle"    "c" 'artist-select-op-circle
     :desc "Draw: ellips"    "e" 'artist-select-op-ellipse
     :desc "Draw: square"    "s" 'artist-select-op-square))))

Elfeed RSS

Setup.

(after! elfeed
  (setq rmh-elfeed-org-files '("~/Dropbox/org/feeds.org")
        rmh-elfeed-org-auto-ignore-invalid-feeds t
        rmh-elfeed-org-ignore-tag "ARCHIVE"
        elfeed-db-directory (t/user-dropbox-folder "Apps/elfeed/")
        elfeed-goodies/entry-pane-position 'right
        elfeed-search-filter "@2-week-ago -youtube -news -tech +unread")
  (add-hook! 'elfeed-db-update-hook 'elfeed-db-save))

Switch around mappings

Switch around the refresh mappings, for more useful defaults.

(after! elfeed
  (map!
   :map elfeed-search-mode-map
   :n "S" (cmd! (call-interactively 'ellama-summarize-webpage)
                (with-current-buffer "*elfeed-search*"
                  (elfeed-search-untag-all-unread)))
   :n "!" #'elfeed-search-untag-all-unread
   :n "?" #'elfeed-search-tag-all-unread
   ;; switcharoo
   :n "gR" #'elfeed-search-update--force
   :n "gr" #'elfeed-search-fetch
   :map elfeed-show-mode-map
   :n "gr" #'elfeed-show-refresh
   ))

Also allow refresh when viewing a post. E.g. to show with images again after toggling them off.

(after! elfeed
  (map!
   :map elfeed-show-mode-map
   :n "gr" #'elfeed-show-refresh
   ))

Tag hydra

(after! elfeed
  (defun t/toggle-elfeed-tag (tag)
    (interactive "sTag: ")
    (when tag
      (setq elfeed-search-filter
            (cond
             ((s-contains? (concat "+" tag) elfeed-search-filter)
              (replace-regexp-in-string (concat "\\+" tag) (concat "-" tag) elfeed-search-filter))
             ((s-contains? (concat "-" tag) elfeed-search-filter)
              (replace-regexp-in-string (concat "-" tag) (concat "+" tag) elfeed-search-filter))
             (t (concat elfeed-search-filter " +" tag))))
      (elfeed-search-update :force)))
  (map!
   :map elfeed-search-mode-map
   :localleader "t" (cmd!
                     (let* ((items '(("a" "adressa")
                                     ("d" "dev")
                                     ("f" "fun")
                                     ("i" "diy")
                                     ("n" "news")
                                     ("p" "photo")
                                     ("r" "read")
                                     ("s" "stories")
                                     ("t" "tech")
                                     ("u" "unread")
                                     ("y" "youtube"))))
                       (funcall (t/micro-state nil
                                               "a" (cmd! (t/toggle-elfeed-tag "adressa"))
                                               "d" (cmd! (t/toggle-elfeed-tag "dev"))
                                               "f" (cmd! (t/toggle-elfeed-tag "fun"))
                                               "i" (cmd! (t/toggle-elfeed-tag "diy"))
                                               "n" (cmd! (t/toggle-elfeed-tag "news"))
                                               "p" (cmd! (t/toggle-elfeed-tag "photo"))
                                               "r" (cmd! (t/toggle-elfeed-tag "read"))
                                               "s" (cmd! (t/toggle-elfeed-tag "stories"))
                                               "t" (cmd! (t/toggle-elfeed-tag "tech"))
                                               "u" (cmd! (t/toggle-elfeed-tag "unread"))
                                               "y" (cmd! (t/toggle-elfeed-tag "youtube"))))
                       (message (s-join ", "
                                        (seq-map (lambda (item)
                                                   (let ((index (string-match (car item) (cadr item))))
                                                     (concat (substring (cadr item) 0 index)
                                                             (concat "(" (car item) ")")
                                                             (substring (cadr item) (1+ index)))))
                                                 items)))))))

Readability

(after! elfeed
  (add-hook 'elfeed-search-mode-hook
            (defun t/hook-elfeed-search-mode-hook ()
              (show-paren-mode -1)
              (visual-line-mode -1))))

Make reading smoother. Turn on olivetti-mode to center content. Wrap lines. Bind n, p for nav, that skips to the next item on reaching the end.

(after! elfeed
  (add-hook 'elfeed-show-mode-hook
            (defun t/hook-elfeed-show-mode-hook ()
              (t/variable-pitch-mode 1)
              (olivetti-mode 1)
              (t/start-spray-micro-state (lambda () (equal major-mode 'elfeed-show-mode))))))

xkcd

Show the mouseover text for xkcd comics by moving to the image and fetching the 'shr-alt text property that holds the mouse over text. Insert it in the buffer on the next tick, to wait for the image to appear first.

(after! elfeed
  (add-hook 'elfeed-show-mode-hook
            (defun t/elfeed-show-xkcd-mouseover-hook ()
              (run-at-time "5 sec" nil
                           (cmd!
                            (when (and
                                   elfeed-show-entry
                                   (s-equals-p "xkcd.com" (car (elfeed-entry-id elfeed-show-entry))))
                              (save-excursion
                                (forward-line)
                                (read-only-mode -1)
                                (when-let ((text (get-text-property (point) 'shr-alt)))
                                  (goto-char (point-max))
                                  (insert "\n\n")
                                  (insert text))
                                (read-only-mode 1))))))))

Auto tagging

Auto tagging of some types of subs.

(after! elfeed
  (add-hook 'elfeed-new-entry-hook
            (elfeed-make-tagger :feed-url "youtube\\.com"
                                :add '(video youtube))))

(after! elfeed
  (add-hook 'elfeed-new-entry-hook
            (elfeed-make-tagger :before "2 weeks ago"
                                :remove 'unread)))

Customize headings

(after! elfeed
  (set-face-attribute 'elfeed-search-tag-face nil :foreground (face-attribute 'font-lock-type-face :foreground))
  (set-face-attribute 'elfeed-search-title-face nil :bold nil :foreground (face-attribute 'font-lock-comment-face :foreground))
  (set-face-attribute 'elfeed-search-unread-title-face nil :bold t :foreground (face-attribute 'font-lock-keyword-face :foreground))
  (copy-face 'elfeed-search-tag-face 'elfeed-hl-face)
  (set-face-attribute 'elfeed-hl-face nil :bold t))

Window placement

(after! elfeed
  ;; this is the start of *elfeed-entry-<title>* names of youtube buffers without an elfeed entry
  (set-popup-rule! "^\\*elfeed-entry-<" :side 'right :size 0.6 :select t :vslot 10)
  (set-popup-rule! "^\\*elfeed-entry*" :side 'right :size 0.6 :select t :vslot 5))

Youtube feed extraction

Extracted youtube feeds like this

curl https://www.youtube.com/@$1 | grep -ioE “<link [^>]+>” | rg rss | sed -E ‘s#.*href=”([^”]+)”.*#\1#’)

Elfeed tube

Elfeed textual youtube support

(use-package! elfeed-tube
  :commands (elfeed-tube-setup)
  :init
  (setq elfeed-tube-auto-save-p nil)
  (setq elfeed-tube-auto-fetch-p t))

(after! elfeed
  (add-hook! 'elfeed-show-mode-hook 'elfeed-tube-setup))

Elfeed entry heading order

Inspiration https://gist.github.com/alphapapa/80d2dba33fafcb50f558464a3a73af9a

(after! elfeed
  (defun t-elfeed-print-entry (&optional entry)
    "Customize what heading goes where, elfeed-search-print-entry--default."

    (let* ((tags (or (elfeed-entry-tags entry) ""))
           (date (elfeed-search-format-date (elfeed-entry-date entry)))
           (feed (elfeed-entry-feed entry))
           (feed-title
            (when feed
              (or (elfeed-meta feed :title) (elfeed-feed-title feed))))
           (title (or (elfeed-meta entry :title) (elfeed-entry-title entry) ""))
           (title-faces (elfeed-search--faces (elfeed-entry-tags entry))))
      (insert (propertize (elfeed-format-column date 10 :left) 'face 'elfeed-search-date-face) " ")
      (when feed-title
        (insert (propertize (elfeed-format-column feed-title 18 :left)
                            'face
                            'elfeed-search-feed-face) " "))
      (insert (propertize (elfeed-format-column title 80 :left)
                          'face
                          (if (and (member 'unread (append tags))
                                   (member 'read (append tags)))
                              'elfeed-hl-face
                            title-faces)
                          'kbd-help title) " ")
      (insert (propertize (elfeed-format-column tags 30 :right)
                          'face 'elfeed-search-date-face) " ")))
  (setq elfeed-search-print-entry-function 't-elfeed-print-entry))
(after! elfeed
  (defun t/show-elfeed-heading-in-minibuffer ()
    (interactive)
    (let ((selected (car (elfeed-search-selected))))
      (when-let ((feed (and selected (elfeed-entry-feed selected))))
        (message "%s: %s"
                 (propertize (elfeed-feed-title feed) 'face 'elfeed-search-feed-face)
                 (propertize (elfeed-entry-title selected) 'face 'elfeed-hl-face)))))
  (defun t/setup-elfeed-heading-in-minibuffer ()
    (interactive)
    (add-hook! 'post-command-hook :local 't/show-elfeed-heading-in-minibuffer))
  (add-hook! 'elfeed-search-mode-hook 't/setup-elfeed-heading-in-minibuffer))

Calendar

Colors

Ready some fonts that stand out.

(after! calendar
  (copy-face font-lock-comment-face 'calendar-week-face)
  (copy-face font-lock-string-face 'calendar-today-face)
  (set-face-attribute 'holiday nil :foreground "VioletRed1" :weight 'bold :background nil)
  (set-face-attribute 'calendar-today-face nil :weight 'bold :background nil)
  (set-face-attribute 'calendar-week-face nil :foreground "VioletRed4"))

Norwegian time

Weeks on start on monday in Norway, and weeks have numbers. I also like holidays.

(after! calendar
  (setq calendar-date-style 'iso
        calendar-week-start-day 1
        calendar-mark-holidays-flag t
        calendar-today-marker 'calendar-today-face
        calendar-intermonth-header '(propertize "w" 'font-lock-face 'calendar-week-face)
        calendar-intermonth-text '(propertize
                                   (format "%2d" (car
                                                  (calendar-iso-from-absolute
                                                   (calendar-absolute-from-gregorian
                                                    (list month day year)))))
                                   'font-lock-face
                                   'calendar-week-face)))

Mark today

Mark today when scrolling past it.

(after! calendar
  (add-hook 'calendar-today-visible-hook 'calendar-mark-today))

Make it norwegian

Translate days and seasons to norwegian.

(after! calendar
  (add-hook 'calendar-initial-window-hook
            (defun t/calendar-initial-window-hook ()
              (require 'calendar-norway)
              (setq calendar-day-header-array ["" "ma" "ti" "on" "to" "fr" ""]
                    calendar-day-name-array ["Søndag" "Mandag" "Tirsdag" "Onsdag" "Torsdag" "Fredag" "Lørdag"]
                    solar-n-hemi-seasons '("Vårjevndøgn"  "Sommersolverv" "Høstjevndøgn" "Vintersolherv")
                    calendar-holidays (append
                                       calendar-norway-raude-dagar
                                       calendar-norway-andre-merkedagar
                                       calendar-norway-dst
                                       '((holiday-fixed 3 17 "St. Patricksdag")
                                         (holiday-fixed 10 31 "Halloween")
                                         (holiday-float 11 4 4 "Thanksgiving"))))
              (calendar-redraw))))

Navigation

Evil like navigation.

(after! calendar
  (add-hook! 'calendar-mode-hook
    (defun t/calendar-mode-hook ()
      (map!
       :map calendar-mode-map
       :m "H"   #'calendar-scroll-left
       :m "L"   #'calendar-scroll-right))))

Re-builder

(after! re-builder
  (setq reb-re-syntax 'rx)
  (defvar t-regex-mode nil "reb-mode on or not"))

(defun t/toggle-regex-mode ()
  (interactive)
  (if t-regex-mode (reb-quit) (re-builder))
  (setq t-regex-mode (not t-regex-mode)))

Email

Settings from modules/email/mu4e/README.org

Inspiration:

Mu4e

(after! mu4e

  (set-popup-rule! "^*mu4e-main" :ignore t)
  (set-popup-rule! "^*mu4e-draft" :ignore nil :side 'right :size 0.8)
  (set-popup-rule! "^*mu4e-headers" :ignore t :side 'right :size 0.8 :vslot 5 :select t)
  (set-popup-rule! "^*mu4e-article" :ignore t :side 'right :size 0.8 :vslot 10 :select t)

  (add-hook! 'mu4e-main-mode-hook
    (defun t/mu4e-main-mode-hook ()
      (interactive)
      (visual-line-mode -1)))

  (setq mu4e-maildir "~/.maildir"
        mu4e-mu-version "1.12.5"
        +mu4e-workspace-name "*email*"
        +mu4e-alert-bell-cmd nil ;; no sounds
        mu4e-split-view 'vertical
        mu4e-update-interval (* 5 60)
        mu4e-context-policy nil
        mu4e-attachment-dir "~/Desktop"
        mu4e-mu-binary (executable-find "mu")
        mu4e-get-mail-command (concat (executable-find "mbsync") " -a")
        mu4e-headers-fields '((:account-stripe . 1)
                              (:human-date . 8)
                              (:from . 22)
                              (:flags . 6)
                              (:subject)
                              (:to . 25))

        ;; rename files when moving - needed for mbsync:
        mu4e-change-filenames-when-moving t
        mu4e-maildir-shortcuts '(("/gmail/INBOX" . ?g)
                                 ("/gmail/[Gmail]/Sent Mail" . ?G)
                                 ("/junk/INBOX" . ?j)
                                 ("/junk/[Gmail]/Sent Mail" . ?J)))

  (add-to-list 'mu4e-bookmarks '(:name "Junk"  :query "maildir:/junk/INBOX" :key ?j))
  (add-to-list 'mu4e-bookmarks '(:name "Gmail" :query "maildir:/gmail/INBOX" :key ?g))

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

  (setq +mu4e-gmail-accounts `((,user-mail-address   . "/gmail")
                               (,user-mail-address-2 . "/junk")))

  (set-email-account!
   "gmail"
   `((mu4e-sent-folder       . "/gmail/[Gmail]/Sent Mail")
     (mu4e-drafts-folder     . "/gmail/[Gmail]/Drafts")
     (mu4e-trash-folder      . "/gmail/[Gmail]/Trash")
     (mu4e-compose-signature . "\nT")
     (user-full-name         . ,user-full-name)
     (user-mail-address      . ,user-mail-address)
     (message-sendmail-extra-arguments . ("--read-envelope-from" "--account=gmail"))
     (org-msg-signature      . "\n\n#+begin_signature\n--\n\nT\n#+end_signature"))
   t)

  (set-email-account!
   "junk"
   `((mu4e-sent-folder       . "/junk/[Gmail]/Sent Mail")
     (mu4e-drafts-folder     . "/junk/[Gmail]/Drafts")
     (mu4e-trash-folder      . "/junk/[Gmail]/Trash")
     (mu4e-compose-signature . "\ntorg")
     (user-full-name         . "torg")
     (user-mail-address      . ,user-mail-address-2)
     (message-sendmail-extra-arguments . ("--read-envelope-from" "--account=junk"))
     (org-msg-signature      . "\n\n#+begin_signature\n--\n\ntorg\n#+end_signature"))
   t)

  (setq sendmail-program (executable-find "msmtp")
        send-mail-function #'smtpmail-send-it
        message-sendmail-f-is-evil t
        message-send-mail-function #'message-send-mail-with-sendmail))
Don’t insert line breaks, softwrap
(after! mu4e
  (add-hook 'mu4e-compose-mode-hook 'turn-off-auto-fill))
Don’t wrap lines in message overview
(after! mu4e
  (add-hook! 'mu4e-headers-mode-hook :append
    (defun t/mu4e-headers-mode-hook ()
      (visual-line-mode -1)
      (show-paren-mode -1))))
Cc Bcc
(after! mu4e
  (add-hook 'mu4e-compose-mode-hook
            (defun t/mu-add-cc-and-bcc ()
              "My Function to automatically add Cc & Bcc: headers."
              (save-excursion (message-add-header "Cc:\n"))
              (save-excursion (message-add-header "Bcc:\n")))))
Wrap long lines in emails

Get rid of consecutive newlines and clean the buffer up. Related struggles.

(after! mu4e
  (add-hook! 'mu4e-view-rendered-hook :append
    (defun t/mu4e-rendered-mode-hook ()
      (evil-normal-state)
      (t/start-spray-micro-state)
      (with-current-buffer "*mu4e-article*"
        (t/remove-consecutive-newlines)
        (olivetti-mode)))))
Remove background color in emails
(after! mu4e
  (require 'mu4e-contrib)
  (setq mu4e-html2text-command 'mu4e-shr2text)
  (setq shr-color-visible-luminance-min 60)
  (setq shr-color-visible-distance-min 5)
  (setq shr-use-colors nil)
  (advice-add #'shr-colorize-region :around (defun shr-no-colourise-region (&rest ignore))))
Troubleshooting

Sometimes mu4e cannot be found, so SPC o C m does not launch it. Try *Doom env from terminal, including SSH_* and GPG_* env vars and try launching =mu4e again.

Setup

Create the folders

The subfolder gmail is what makes the mu4e setting above need /gmail/ in their path

mkdir -p ~/.maildir/{gmail,junk}
Export certificates
  • On arch or nix on mac: /etc/ssl/certs/ca-certificates.crt
  • On plainmacos: Keychain Access -> System Roots -> Certificates -> select all -> Shift+cmd+E to ~~/.maildir/certificates/certificates.pem~
Delete to Trash

Gmail setting to remove deleted emails from inbox and move them in [Gmail]/Trash.

Go to Gmail IMAP/POP settings (in normal view; the options are not available in HTML view) and set “When a message is marked as deleted” to “Move to trash” or “Immediately delete.” Also set “Auto-Expunge off.”

Configuration files

Smtp

brew install msmtp

Create .msmtprc config for sending email

(after! f
  (f-write-text
   (concat "defaults
auth on
port 587
protocol smtp
tls on
tls_starttls on

account gmail
host smtp.gmail.com
from " (getenv "USER_EMAIL") "
user " (replace-regexp-in-string "@gmail.com" "" (getenv "USER_EMAIL")) "
passwordeval \"gpg -q --for-your-eyes-only --no-tty --logger-file /dev/null --batch -d ~/.authinfo.gpg | awk '/machine smtp\\.gmail\\.com login "
(replace-regexp-in-string "\\." "\\\\." (getenv "USER_EMAIL"))
" port 587/ {print $NF}'\"

account junk
host smtp.gmail.com
from " (getenv "USER_EMAIL_2") "
user " (replace-regexp-in-string "@gmail.com" "" (getenv "USER_EMAIL_2")) "
passwordeval \"gpg -q --for-your-eyes-only --no-tty --logger-file /dev/null --batch -d ~/.authinfo.gpg | awk '/machine smtp\\.gmail\\.com login "
(replace-regexp-in-string "\\." "\\\\." (getenv "USER_EMAIL_2"))
" port 587/ {print $NF}'\"
")
   'utf-8 (t/user-file ".msmtprc")))
Mbsync

brew install isync

(after! f
  (f-write-text
   (concat "
 # gmail ========================
 IMAPAccount gmail
 Host imap.gmail.com
 User " (getenv "USER_EMAIL") "
 PassCmd \"gpg -q --for-your-eyes-only --no-tty --logger-file /dev/null --batch -d ~/.authinfo.gpg | awk '/machine imap\\.gmail\\.com login "
 (replace-regexp-in-string "\\." "\\\\." (getenv "USER_EMAIL"))
 " port 993/ {print $NF}'\"
 Port 993
 SSLType IMAPS
 SSLVersions TLSv1.2
 AuthMechs PLAIN
 SystemCertificates no
 CertificateFile /etc/ssl/certs/ca-certificates.crt

 IMAPStore gmail-remote
 Account gmail

 MaildirStore gmail-local
 SubFolders Verbatim
 Path ~/.maildir/gmail/
 Inbox ~/.maildir/gmail/INBOX

 Channel gmail
 Far :gmail-remote:
 Near :gmail-local:
 Patterns * ![Gmail]* \"[Gmail]/Sent Mail\" \"[Gmail]/Trash\"
 Create Near
 Sync All
 Expunge Both
 SyncState *

 # junk =========================
 IMAPAccount junk
 Host imap.gmail.com
 User " (getenv "USER_EMAIL_2") "
 PassCmd \"gpg -q --for-your-eyes-only --no-tty --logger-file /dev/null --batch -d ~/.authinfo.gpg | awk '/machine imap\\.gmail\\.com login "
 (replace-regexp-in-string "\\." "\\\\." (getenv "USER_EMAIL_2"))
 " port 993/ {print $NF}'\"
 Port 993
 SSLType IMAPS
 SSLVersions TLSv1.2
 AuthMechs PLAIN
 SystemCertificates no
 CertificateFile /etc/ssl/certs/ca-certificates.crt

 IMAPStore junk-remote
 Account junk

 MaildirStore junk-local
 SubFolders Verbatim
 Path ~/.maildir/junk/
 Inbox ~/.maildir/junk/INBOX

 Channel junk
 Far :junk-remote:
 Near :junk-local:
 Patterns * ![Gmail]* \"[Gmail]/Sent Mail\" \"[Gmail]/Trash\"
 Create Near
 Sync All
 Expunge Both
 SyncState *")
   'utf-8 (t/user-file ".mbsyncrc")))

Then sync it with mbsync -aV.

Mu

First time mu initialization

(shell-command-to-string
 (concat "mu init -m ~/.maildir"
         " --my-address " (getenv "USER_EMAIL")
         " --my-address " (getenv "USER_EMAIL_2")
         " && mu index"))

Keybindings

(after! mu4e
  (map! :map mu4e-headers-mode-map
        :n "*" #'mu4e-headers-mark-for-something
        :n "u" #'mu4e-headers-mark-for-read
        :n "U" #'mu4e-headers-mark-for-unread
        :n "!" #'mu4e-headers-mark-for-unmark
        :n "?" #'mu4e-mark-unmark-all))

Had to locate the mu4e installed nix package

fd / mu4e

and add it to the load path of emacs manually, something like this

(add-to-list ‘load-path ”nix/store/g119kihhviy5c4krzr8h30wakzldab3d-mu-1.10.8-mu4e/share/emacs/site-lisp/mu4e”)

Site lisp

Useful elisp I committed, or decided to work on.

(after! org
  (use-package! ox-gfm
    :commands org-export-dispatch
    :load-path (lambda () (t/user-emacs-file "site-lisp/ox-gfm/"))))
(use-package! consult-async
  :commands consult-web-search
  :load-path (lambda () (t/user-emacs-file "site-lisp/consult-async/")))
;; don't use this for large files, e.g. like 15MB, it really brings emacs to a stall
(use-package! nxml-eldoc
  :commands turn-on-nxml-eldoc
  :load-path (lambda () (t/user-emacs-file "site-lisp/nxml-eldoc/")))
(use-package! json-path-eldoc
  :commands turn-on-json-path-eldoc
  ;; TODO too slow on large files, use treesitter?
  ;;:init (add-hook! 'json-mode-hook 'turn-on-json-path-eldoc)
  :load-path (lambda () (t/user-emacs-file "site-lisp/json-path-eldoc/")))
(defun t/json-mode-hook ()
  (interactive)
  (eldoc-mode -1))
(add-hook! 'json-mode-hook #'t/json-mode-hook)
(remove-hook! 'json-mode-hook #'+electric--init-json-mode-h)

Idle highlight across in all buffers

A little minor mode to highlight the symbol at point or the selected region across all emacs buffers.

(defvar t-idle-highlight-idle-time 0.3
  "Time before idle highlight kicks in.")

(defvar t-idle-highlight-flash-duration 1.5
  "Time of idle highlight flash duration.")

(defvar t-idle-highlight--timer nil
  "Idle timer that triggers the highlight when user is idle.")

(define-minor-mode t-idle-highlight-mode
  "Minor mode that will highlight symbol at point when emacs is idle."
  :init-value nil
  :global t
  :lighter " t-idle-highlight"
  (if t-idle-highlight-mode
      (progn
        (when (not (featurep 'highlight-symbol))
          (require 'highlight-symbol))
        (add-hook 'pre-command-hook 't/unhighlight-in-all-buffers)
        (setq t-idle-highlight--timer
              (run-with-idle-timer
               t-idle-highlight-idle-time
               t
               't/highlight-in-all-buffers)))
    (progn
      (remove-hook 'pre-command-hook 't/unhighlight-in-all-buffers)
      (when t-idle-highlight--timer
        (cancel-timer t-idle-highlight--timer)
        (setq t-idle-highlight--timer nil)
        (t/unhighlight-in-all-buffers)))))

(defface t-idle-highlight-face
  '((t (:weight bold)))
  "Idle highlighting face.")

(defface t-idle-highlight-face-loud
  '((t (:weight bold :background "#f0a" :foreground "#dadada")))
  "Idle highlighting face, loud.")

(defun t/flash-in-all-buffers ()
  "Highlight symbol at point across all buffers temporarily."
  (interactive)
  (t/highlight-in-all-buffers 't-idle-highlight-face-loud)
  (run-at-time (format "%d sec" t-idle-highlight-flash-duration) nil 't/unhighlight-in-all-buffers))

(defun t/highlight-with-all-buffers (fn)
  (if-let ((symbol
            (if (use-region-p)
                (buffer-substring-no-properties (region-beginning) (region-end))
              (thing-at-point 'symbol t))))
      (save-excursion
        (let ((tail (buffer-list)))
          (while tail
            (let ((buffer (car tail)))
              (set-buffer buffer)
              (funcall fn symbol)
              (setq tail (cdr tail))))))))

(defun t/highlight-in-all-buffers (&optional face)
  "Highlight symbol at point in all buffers."
  (interactive)
  (t/highlight-with-all-buffers
   (lambda (symbol)
     (highlight-symbol-add-symbol-with-face symbol (or face 't-idle-highlight-face)))))

(defun t/unhighlight-in-all-buffers ()
  "Remove all symbol highlights in all buffers."
  (interactive)
  (t/highlight-with-all-buffers
   (lambda (symbol)
     (highlight-symbol-remove-all))))

Extend evil to flash highlights on SPC u *

(after! evil
  (map! :map evil-motion-state-map
        "*" (cmd! (when (not (featurep 'highlight-symbol))
                    (require 'highlight-symbol))
                  (if (t/prefix-arg-universal?)
                      (t/flash-in-all-buffers)
                    (call-interactively 'evil-ex-search-word-forward)))))

Bugfixes

compat-assoc compat-assoc-delete-all not defined?

(when is-linux
  (defun compat-assoc (&optional a b c) nil))
(when is-linux
  (defun compat-assoc-delete-all (&optional a b c) nil))

Stuff to test

Run in every file opened

(add-hook 'find-file-hook
          (defun t/in-every-file ()
            ;;(when (string= (file-name-extension buffer-file-name) "ts") (typescript-mode))
            ))

Sticky buffer mode

Useful e.g. to make dired act like a directory tree sidebar

(define-minor-mode sticky-buffer-mode
  "Make the current window always display this buffer."
  nil " sticky" nil
  (set-window-dedicated-p (selected-window) sticky-buffer-mode)
  (setq window-size-fixed (if sticky-buffer-mode 'width nil)))

gh run watch

(defun t/gha ()
  (interactive)
  (let ((current-prefix-arg 1))
    ;; (call-interactively '+vterm/here)
    (call-interactively '+vterm/toggle))
  (term-send-raw-string "gh run watch\C-m"))

xref jump to selection

(comment

 (progn

   (require 'xref)
   (let ((l (xref-location-marker
             (xref-make-file-location
              (t/user-emacs-file "test-files/index.js")
              27
              11))))
     (xref--show-pos-in-buf l (marker-buffer l))))
 )

Linux

Reload sway

Check if configuration affecting sway is changed, if so reload it

(add-hook! 'after-save-hook
  (defun t/reload-sway-after-save ()
    (when
        (or (s-ends-with? "i3status-rust/config.toml" (buffer-file-name))
            (s-ends-with? "config/sway/config" (buffer-file-name)))
      (call-process-shell-command "swaymsg reload"))))

Someday

(doom-mark-buffer-as-real-h) can make scratch buffer navigable with s-j/k

deno-emacs

June 18, 2021 at 12:12PM https://ift.tt/36jQLT2

deno-fmt is function that formats the current buffer on save with deno fmt. The package also exports a minor mode that applies (deno-fmt) on save. Feel free to replace typescript-mode / js2-mode in the following with your TypeScript/JavaScript mode of choice.

(after! eglot

  (defclass eglot-deno (eglot-lsp-server) ()
    :documentation "A custom class for deno lsp.")

  (cl-defmethod eglot-initialization-options ((server eglot-deno))
    "Passes through required deno initialization options"
    (list :enable t
          :lint t)))

emms mpd

(after! emms
  (setq emms-player-list (list 'emms-player-mpd))
  (add-to-list 'emms-info-functions (list 'emms-info-mpd))
(emms-cache-set-from-mpd-all))

Then type C-ret to add something to the playlist.

(after! emms
  (setq emms-volume-change-amount 10
        emms-volume-change-function 'emms-volume-mpd-change)
  (defun t/music-volume-up     () (interactive) (emms-volume-raise))
  (defun t/music-volume-down   () (interactive) (emms-volume-lower))
  (defun t/music-browse        () (interactive) (emms-smart-browse))
  (defun t/music-stop          () (interactive) (emms-stop))
  (defun t/music-seek-backward () (interactive) (emms-seek-backward))
  (defun t/music-seek-forward  () (interactive) (emms-seek-forward))
  (defun t/music-play-pause    () (interactive) (if emms-player-playing-p
                                                    (emms-pause)
                                                  (emms-start)))
  (defun t/music-prev          () (interactive) (emms-previous))
  (defun t/music-next          () (interactive) (emms-next))
  (defun t/music-browse        () (interactive) (emms-smart-browse)))

copilot.el

(use-package! copilot
  :bind (:map copilot-completion-map
              ("<tab>"   . 'copilot-accept-completion)
              ("TAB"     . 'copilot-accept-completion)
              ("C-TAB"   . 'copilot-accept-completion-by-word)
              ("C-<tab>" . 'copilot-accept-completion-by-word)))

Temporarily replace nav-flash with pulsar.el

Unload module

(when (featurep 'nav-flash)
  (unload-feature 'nav-flash))
(use-package! pulsar
  :commands pulsar-global-mode
  :init
  (setq pulsar-pulse t)
  (setq pulsar-delay 0.055)
  (setq pulsar-iterations 10)
  (setq pulsar-face 'pulsar-magenta)
  (setq pulsar-highlight-face 'pulsar-yellow)
  (add-hook! '(imenu-after-jump-hook
               better-jumper-post-jump-hook
               counsel-grep-post-action-hook
               consult-after-jump-hook
               dumb-jump-after-jump-hook)
             #'pulsar-pulse-line)

  (add-hook 'doom-switch-window-hook #'pulsar-pulse-line)

  ;; `org'
  (add-hook 'org-follow-link-hook
            (cmd!
             (run-at-time 0.1 nil #'pulsar-pulse-line)))

  ;; `saveplace'
  (advice-add #'save-place-find-file-hook :after #'pulsar-pulse-line)

  ;; `evil'
  (advice-add #'evil-window-top    :after #'pulsar-pulse-line)
  (advice-add #'evil-window-middle :after #'pulsar-pulse-line)
  (advice-add #'evil-window-bottom :after #'pulsar-pulse-line)

  ;; Bound to `ga' for evil users
  (advice-add #'what-cursor-position :after #'pulsar-pulse-line)
  :config
  (pulsar-global-mode 1))

alert.el

(setq alert-default-style (if is-mac 'osx-notifier 'libnotify))

ollama

(defun t/ellama-code-ask (prompt)
  (interactive "sPrompt: ")
  (let* ((snippet (when (region-active-p)
                    (buffer-substring-no-properties (region-beginning) (region-end))))
         (json (json-serialize (list
                                'model "codellama"
                                'prompt
                                (if snippet
                                    (concat "# " prompt "\n\n" snippet)
                                  prompt)))))
    (t/async-shell-command
     "ChatGPT"
     (format
      "curl --no-buffer -s http://torgnix:11434/api/generate --data-binary @- <<EOF | jq --unbuffered -jrc '.response'
%s
EOF" (replace-regexp-in-string "`" "\\\\`" json)) ;; bash cant handle ` in EOF?
     )
    (pop-to-buffer "*ChatGPT*")))

(defun t/ellama-review-code ()
  (interactive)
  (t/ellama-code-ask
   (concat
    "Review the following code and make concise suggestions. "
    (if (t/prefix-arg-universal?) (read-string "Be specific: ") ""))))

(use-package! ellama
  :config
  (setq torgnix-providers
        (list
         (make-llm-ollama
          :scheme "http"
          :host "torgnix"
          :port 11434
          :chat-model "llama3"
          :embedding-model "llama3")
         (make-llm-ollama
          :scheme "http"
          :host "torgnix"
          :port 11434
          :chat-model "wizard-vicuna-uncensored:13b"
          :embedding-model "wizard-vicuna-uncensored:13b")))
  (setq ellama-providers nil)
  (add-to-list 'ellama-providers `("torgnix llama" . ,(car torgnix-providers)))
  (add-to-list 'ellama-providers `("torgnix wizard" . ,(cadr torgnix-providers)))
  (setq ellama-provider (car torgnix-providers))
  (setq ellama-keymap-prefix "C-c")
  (ellama-setup-keymap)
  (set-popup-rule! "^ellama"
    :side 'right
    :size 0.7
    :vslot 2
    :select t)
  (map! :leader
        (:prefix
         ("o" . "open")
         (:prefix-map
          ("g" . "ollama")
          (:when t
            :desc "code-complete" "c c" 'ellama-code-complete
            :desc "code-add" "c a" 'ellama-code-add
            :desc "code-edit" "c e" 'ellama-code-edit
            :desc "code-improve" "c i" 'ellama-code-improve
            :desc "code-review" "c r" 'ellama-code-review
            ;; summarize
            :desc "summarize" "s s" 'ellama-summarize
            :desc "summarize-webpage" "s w" 'ellama-summarize-webpage
            ;; session
            :desc "load-session" "s l" 'ellama-load-session
            :desc "session-rename" "s r" 'ellama-session-rename
            :desc "session-remove" "s d" 'ellama-session-remove
            :desc "session-switch" "s a" 'ellama-session-switch
            ;; improve
            :desc "improve-wording" "i w" 'ellama-improve-wording
            :desc "improve-grammar" "i g" 'ellama-improve-grammar
            :desc "improve-conciseness" "i c" 'ellama-improve-conciseness
            ;; make
            :desc "make-list" "m l" 'ellama-make-list
            :desc "make-table" "m t" 'ellama-make-table
            :desc "make-format" "m f" 'ellama-make-format
            ;; ask
            :desc "ask-about" "a a" 'ellama-ask-about
            :desc "chat" "a i" 'ellama-chat
            :desc "ask-line" "a l" 'ellama-ask-line
            :desc "ask-selection" "a s" 'ellama-ask-selection
            ;; text
            :desc "translate" "t t" 'ellama-translate
            :desc "translate-buffer" "t b" 'ellama-translate-buffer
            :desc "complete" "t c" 'ellama-complete
            :desc "chat-translation-enable" "t e" 'ellama-chat-translation-enable
            :desc "chat-translation-disable" "t d" 'ellama-chat-translation-disable
            ;; define
            :desc "define-word" "d w" 'ellama-define-word
            ;; context
            :desc "context-add-buffer" "x b" 'ellama-context-add-buffer
            :desc "context-add-file" "x f" 'ellama-context-add-file
            :desc "context-add-selection" "x s" 'ellama-context-add-selection
            :desc "context-add-info-node" "x i" 'ellama-context-add-info-node
            ;; provider
            :desc "provider-select" "p s" 'ellama-provider-select
            )))))
(after! ellama
  (defun ellama-summarize-webpage (url)
    "Summarize webpage fetched from URL."
    (interactive
     (list
      (if-let ((url (or (and (fboundp 'thing-at-point) (thing-at-point 'url))
                        (shr-url-at-point nil)
                        (org-element-property :raw-link (org-element-context))
                        (and (equal major-mode 'elfeed-show-mode)
                             (elfeed-entry-link elfeed-show-entry))
                        (and (equal major-mode 'elfeed-search-mode)
                             (elfeed-entry-link (car (elfeed-search-selected)))))))
          url
        (read-string "Enter URL you want to summarize: "))))
    (let ((buffer-name (url-retrieve-synchronously url t)))
      ;; (display-buffer buffer-name)
      (with-current-buffer buffer-name
        (goto-char (point-min))
        (or (search-forward "<!DOCTYPE" nil t)
            (search-forward "<html" nil))
        (beginning-of-line)
        (kill-region (point-min) (point))
        (shr-insert-document (libxml-parse-html-region (point-min) (point-max)))
        (goto-char (point-min))
        (or (search-forward "<!DOCTYPE" nil t)
            (search-forward "<html" nil))
        (beginning-of-line)
        (kill-region (point) (point-max))
        (ellama-summarize)))))

default workspaces

(after! workspaces
  (advice-add '+workspace/kill-session :before 'eglot-shutdown-all))
(defun t/+workspace-new (name &rest buffers)
  (interactive)
  (+workspace-switch name t)
  (dolist (buffer (reverse buffers))
    (find-file buffer)))
(defun t/default-workspaces-init ()
  (interactive)
  (t/+workspace-new "doom" doom-user-dir)
  (t/+workspace-new "dot" "~/Projects/dotfiles")
  (t/+workspace-new "nix-darwin" "~/.config/nix-darwin/" "~/.config/nix-darwin/flake.nix" "~/.config/nix-darwin/home/default.nix")
  (t/+workspace-new "org" (format "%s/home.org.gpg" (car org-agenda-files))))
(defun t/default-workspaces ()
  (interactive)
  (if (equal '("main") (+workspace-list-names))
      (t/default-workspaces-init)
    (message "Workspaces exist, bailing: %s" (+workspace-list-names))))