Skip to content

Latest commit

 

History

History
3274 lines (2665 loc) · 99.7 KB

File metadata and controls

3274 lines (2665 loc) · 99.7 KB

doom-emacs config

General

User Information

Some functionality uses this to identify you, e.g. GPG configuration, email clients, file templates and snippets.

(setq user-full-name "Stefan Lendl"
      user-mail-address "ste.lendl@gmail.com")

literate config

Don’t auto-recompile literate-config

(remove-hook 'org-mode-hook #'+literate-enable-recompile-h)

private config file

doom/goto-private-config-file goes to config.el which I will never edit by hand. Opening config.org makes much more sense.

(defun stfl/goto-private-config-file ()
  "Open your private config.el file."
  (interactive)
  (find-file (expand-file-name "config.org" doom-user-dir)))

bind it to SPC h d c

(define-key! help-map
      "dc" #'stfl/goto-private-config-file
      "dC" #'doom/open-private-config)

Buffer Settings

;; (global-auto-revert-mode 1)
(setq undo-limit 80000000
      evil-want-fine-undo t
      inhibit-compacting-font-caches t)

Auto-saving all buffers

(setq auto-save-default t)
(run-with-idle-timer 60 t '(lambda () (save-some-buffers t)))

Open External Browser

Use Brave browser if availible. The default should actually respect the $BROWSER env

(when (executable-find "brave")
  (setq! browse-url-browser-function 'browse-url-chromium
         browse-url-chromium-program "brave"))

Resizing window with the mouse

Resizing a window vertically with the mouse in emacs requires you to hit the 1px thick line between windows. This is less of a problem horizontally, because you can drag the modeline.

To make this easier use, Meta + left mouse drag to activate the resizing. This only works if there is a window to the right. Window is resized, after passing the border with the mouse.

(global-set-key [M-drag-mouse-2] #'mouse-drag-vertical-line)

It would be nice to do that on the left fringe! But this would active again resizing to the right…

;; (defun mouse-drag-left-line (start-event)
;;   "Change the width of a window by dragging on a vertical line.
;; START-EVENT is the starting mouse event of the drag action."
;;   (interactive "e")
;;   (mouse-drag-line start-event 'left))

;; (global-set-key [left-fringe drag-mouse-1] #'mouse-drag-left-line)

Motions

limit evil-snipe to the bufffer

(after! evil-snipe
  (setq evil-snipe-scope 'visible
        evil-snipe-repeat-scope 'visible))
(map! :leader "f ." #'find-file-at-point)

Always indent the line when pressing tab regardles of the cursor position. If the line is already correctly indented, try to complete.

(setq! tab-always-indent 'complete)
(after! evil-escape
  (setq evil-escape-key-sequence "jk"))

moving lines up and down (drag-stuff)

(package! drag-stuff)
(use-package! drag-stuff
  :defer t
  :init
  (map! "<M-up>"    #'drag-stuff-up
        "<M-down>"  #'drag-stuff-down
        "<M-left>"  #'drag-stuff-left
        "<M-right>" #'drag-stuff-right))

Garbage Collection

Disable automatic garbage collection and run when emacs is idle.

https://jackjamison.xyz/blog/emacs-garbage-collection/

(setq gc-cons-threshold most-positive-fixnum)

(run-with-idle-timer 1.2 t 'garbage-collect)

Mouse Window and Frame selection

Sway automatically focues a frame when moving the mouse over. Emacs does not register this and mouse clicks are registed on the wrong frame.

(setq! focus-follows-mouse 'auto-raise
       mouse-autoselect-window nil)

Find files and directories using fd

(after! consult
  (setq! consult-fd-args '((if (executable-find "fdfind" 'remote) "fdfind" "fd")
                           "--color=never"
                           ;; "--full-path"
                           ;; "--absolute-path"
                           "--hidden"
                           "--exclude .git"
                           (if (featurep :system 'windows) "--path-separator=/")))

  (defun stfl/consult-fd (&optional arg)
    (interactive "P")
    (let ((consult-fd-args (append consult-fd-args (and arg '("--no-ignore")))))
      (consult-fd)))

  (defun stfl/projectile-find-file (&optional arg)
    (interactive "P")
    (if arg (stfl/consult-fd arg)
      (projectile-find-file)))

  ;; (defun stfl/projectile-find-file (&optional arg)
  ;;   (interactive "P")
  ;;   (let ((projectile-git-fd-args (concat projectile-git-fd-args (and arg " --no-ignore-vcs"))))
  ;;     (projectile-find-file)))

  (defun stfl/consult-fd-dir (&optional arg)
    (interactive "P")
    (let ((consult-fd-args (append consult-fd-args '("--type d") (and arg '("--no-ignore")))))
      (consult-fd)))

  (map! :leader
        "SPC" #'stfl/projectile-find-file
        :prefix "f"
        "l" #'stfl/consult-fd
        "L" #'stfl/consult-fd-dir))

Theme

doom-theme

(setq doom-theme 'doom-one)
(setq! display-line-numbers-type t)
(setq! which-key-idle-delay 0.3)

Font

Doom exposes five (optional) variables for controlling fonts in Doom. Here are the three important ones:

  • `doom-font’
  • `doom-variable-pitch-font’
  • `doom-big-font’ – used for `doom-big-font-mode’; use this for presentations or streaming.

They all accept either a font-spec, font string (“Input Mono-12”), or xlfd font string. You generally only need these two:

(let ((font "JetBrains Mono Nerd Font Mono"))
  (setq doom-font (font-spec :family font :size 13)
        doom-variable-pitch-font (font-spec :family font)
        doom-big-font (font-spec :family font :size 20)))

font detection taken from tecosaur https://tecosaur.github.io/emacs-config/config.html#font-face

(defvar required-fonts
  '("JetBrainsMono.*"
    ;; "Overpass"
    ;; "JuliaMono"
    ;; "IBM Plex Mono"
    ;; "Merriweather"
    ;; "Alegreya"
    ))

(defvar available-fonts
  (delete-dups (or (font-family-list)
                   (split-string (shell-command-to-string "fc-list : family")
                                 "[,\n]"))))

(defvar missing-fonts
  (delq nil (mapcar
             (lambda (font)
               (unless (delq nil (mapcar (lambda (f)
                                           (string-match-p (format "^%s$" font) f))
                                         available-fonts))
                 font))
             required-fonts)))

(if missing-fonts
    (pp-to-string
     `(unless noninteractive
        (add-hook! 'doom-init-ui-hook
          (run-at-time nil nil
                       (lambda ()
                         (message "%s missing the following fonts: %s"
                                  (propertize "Warning!" 'face '(bold warning))
                                  (mapconcat (lambda (font)
                                               (propertize font 'face 'font-lock-variable-name-face))
                                             ',missing-fonts
                                             ", "))
                         (sleep-for 0.5))))))
  ";; No missing fonts detected")

Faces

(custom-set-faces!
  `(whitespace-indentation :background ,(doom-color 'base4)) ; Visually highlight if an indentation issue was discovered which emacs already does for us
  `(magit-branch-current  :foreground ,(doom-color 'blue) :box t)
  '(lsp-inlay-hint-face :height 0.85 :italic t :inherit font-lock-comment-face)
  '(lsp-bridge-inlay-hint-face :height 0.85 :italic t :inherit font-lock-comment-face)
)

tab-width

(setq! tab-width 4)

[#D] Org mode settings

General

Org directory

(setq org-directory "~/.org")

always generate an id for a link

(after! org-id
  (setq org-id-link-to-org-use-id t
        org-id-locations-file (doom-path doom-local-dir "org-id-locations")
        org-id-track-globally t))

rebuild orgid file on start at the first time emacs is idle for 20 sec

(after! org-id (run-with-idle-timer 20 nil 'org-id-update-id-locations))

also rebuild that via org-roam

(after! org-roam (run-with-idle-timer 25 nil 'org-roam-update-org-id-locations))

agile-gtd

;; Local dev — switch to GitHub recipe once published:
(package! agile-gtd
  ;; :recipe (:local-repo "~/work/agile-gtd" :build (:not compile)))
  :recipe (:host github :repo "stfl/agile-gtd.el"))
(use-package agile-gtd
  :after org
  :config
  ;; Additional agenda files not managed by agile-gtd projects
  (setq org-agenda-files (mapcar (lambda (f) (expand-file-name f org-directory))
                                 '("inbox-orgzly.org"
                                   "geschenke.org"
                                   "media.org"
                                   "projects.org"
                                   "versicherung.org"
                                   "ikea.org"
                                   "cafe-glas.org")))
  (setq agile-gtd-projects '((:tag "oebb"    :name "ÖBB"       :key ?o)
                              (:tag "origina" :name "Origina"   :key ?i)
                              (:tag "pulswerk" :name "Pulswerk" :key ?p)
                              (:tag "freelance" :name "Freelance")
                              (:tag "emacs"     :name "Emacs")))
  (agile-gtd-enable)
  )

org-mcp

MCP server for Org-mode, exposing agenda files to AI assistants (e.g. Claude Code).

(package! mcp-server-lib)
(package! org-mcp
  :recipe (:host github :repo "stfl/org-mcp"))
  ;; :recipe  (:local-repo "~/work/org-mcp" :build (:not compile)))
(use-package org-mcp
  :after (org agile-gtd)
  :custom
  (org-mcp-stored-queries-file nil) ;; FIXME rework the stored query functionality
  (org-mcp-ql-extra-properties '((parent-priority . agile-gtd--direct-parent-priority)
                                 (rank . agile-gtd--item-rank)))
  (org-mcp-query-inbox-fn   #'agile-gtd-agenda-query-inbox)
  (org-mcp-query-backlog-fn #'agile-gtd-agenda-query-backlog)
  (org-mcp-query-next-fn    #'agile-gtd-agenda-query-next-actions)
  (org-mcp-query-sort-fn    #'agile-gtd--item-rank<)
  :config
  (if mcp-server-lib--running
      (message "org-mcp: MCP server already running, skipping start")
    (mcp-server-lib-start)))

;; Set org-mcp-allowed-files after agile-gtd-enable has populated org-agenda-files.
;; Must be a separate (after! org …) block registered AFTER the use-package forms
;; above so it fires as a later hook — after agile-gtd-enable completes.
(with-eval-after-load 'org
  (setopt org-mcp-allowed-files
          (mapcar (lambda (f) (expand-file-name f org-directory)) org-agenda-files)))

Appearance

First I like to add some extra fancy stuff to make org-mode more appealing when i’m using +pretty flag.

  • Other options for ellipsis “▼, ↴, ⬎, ⤷,…, and ⋱.”
  • Extra options for headline-bullets-list: “◉” “●” “○” “∴”
(after! org
  (setq! org-auto-align-tags nil
         org-tags-column 0
         org-fold-catch-invisible-edits 'show-and-error
         org-ellipsis ""
         org-indent-indentation-per-level 2)

  (auto-fill-mode))
; (custom-declare-face 'org-checkbox-statistics-todo '((t (:inherit (bold font-lock-constant-face org-todo)))) "")

(custom-set-faces!
  '(org-document-title :foreground "#c678dd" :weight bold :height 1.8)
  '(org-ql-view-due-date :foreground "dark goldenrod")
  `(org-code :foreground ,(doom-lighten (doom-color 'warning) 0.3) :extend t)
  '(outline-1 :height 1.5)
  '(outline-2 :height 1.25)
  '(outline-3 :height 1.15)
  `(org-column :height 130 :background ,(doom-color 'base4)
    :slant normal :weight regular :underline nil :overline nil :strike-through nil :box nil :inverse-video nil)
  `(org-column-title :height 150 :background ,(doom-color 'base4) :weight bold :underline t))

Tag colors

(after! org
  (setq! org-tag-faces `((,agile-gtd-lastmile-tag . (:foreground ,(doom-color 'red) :strike-through t))
                         (,agile-gtd-habit-tag . (:foreground ,(doom-darken (doom-color 'orange) 0.2)))
                         (,agile-gtd-someday-tag . (:slant italic :weight bold))
                         ;; ("finance" . (:foreground "goldenrod"))
                         ;; ("#inbox" . (:background ,(doom-color 'base4) :foregorund ,(doom-color 'base8)))
                         ("#inbox" . (:strike-through t))
                         ("3datax" . (:foreground ,(doom-color 'green)))
                         ("oebb" . (:foreground ,(doom-color 'green)))
                         ("pulswerk" . (:foreground ,(doom-color 'dark-blue)))
                         (,agile-gtd-work-tag . (:foreground ,(doom-color 'blue)))
                         ;; ("#work" . (:foreground ,(doom-color 'blue)))
                         ("@ikea" . (:foreground ,(doom-color 'yellow)))
                         ("@amazon" . (:foreground ,(doom-color 'yellow)))
                         ;; ("emacs" . (:foreground "#c678dd"))
                         ))
  )

Auto-saving org-mode files

Automatically saving all org-buffers when emacs is idle for 30 seconds.

(after! org (run-with-idle-timer 60 t #'org-save-all-org-buffers))

org-mode Startup

(after! org
  (setq org-startup-indented 'indent
        org-startup-folded 'fold
        org-startup-with-inline-images t
        ;; org-image-actual-width (round (* (font-get doom-font :size) 25))
        org-image-actual-width (* (default-font-width) 40)
        ))
(add-hook 'org-mode-hook 'org-indent-mode)
;; (add-hook 'org-mode-hook 'turn-off-auto-fill)

See doomemacs/doomemacs#3185 - Invalid base64 data

(defadvice! no-errors/+org-inline-image-data-fn (_protocol link _description)
  :override #'+org-inline-image-data-fn
  "Interpret LINK as base64-encoded image data. Ignore all errors."
  (ignore-errors
    (base64-decode-string link)))

[#C] Key Bindings

From here we load some extra key bindings that I use often

;; (bind-key "<f6>" #'link-hint-copy-link)
(map! :after org
      :map org-mode-map
      :leader
      :prefix ("n" . "notes")
      :desc "Revert all org buffers" "R" #'org-revert-all-org-buffers
      )

(map! :after org
      :map org-mode-map
      :localleader
      :desc "Revert all org buffers" "R" #'org-revert-all-org-buffers
      "N" #'org-add-note

      :prefix ("l" . "links")
      "o" #'org-open-at-point
      "g" #'eos/org-add-ids-to-headlines-in-file

      :prefix ("d" . "dates/deadlines")
      "c" #'org-cancel-repeater
      )

Refiling

refile target -> build list of someday files dynamically

This is configured in agile-gtd.

refile to roam files by

(defun stfl/build-my-roam-files () (file-expand-wildcards (doom-path org-directory "roam/**/*.org")))

(defun stfl/refile-to-roam ()
  (interactive)
  (let ((org-refile-targets '((stfl/build-my-roam-files :maxlevel . 1))))
    (call-interactively 'org-refile)))

Creating an org-roam note from an existing headline

(defun org-roam-create-note-from-headline ()
  "Create an Org-roam note from the current headline and jump to it.

Normally, insert the headline’s title using the ’#title:’ file-level property
and delete the Org-mode headline. However, if the current headline has a
Org-mode properties drawer already, keep the headline and don’t insert
‘#+title:'. Org-roam can extract the title from both kinds of notes, but using
‘#+title:’ is a bit cleaner for a short note, which Org-roam encourages."
  (interactive)
  (let ((title (nth 4 (org-heading-components)))
        (has-properties (org-get-property-block)))
    (org-cut-subtree)
    (org-roam-find-file title nil nil 'no-confirm)
    (org-paste-subtree)
    (unless has-properties
      (kill-line)
      (while (outline-next-heading)
        (org-promote)))
    (goto-char (point-min))
    (when has-properties
      (kill-line)
      (kill-line))))

Capture Templates

(with-eval-after-load 'org
  (setq org-capture-templates
        (append
         (cl-remove-if (lambda (template)
                         (equal "v" (car-safe template)))
                       org-capture-templates)
         `(("v" "Versicherung" entry
            (file+headline ,(doom-path org-directory "versicherung.org") "Einreichungen")
            (function stfl/org-capture-template-versicherung)
            :root "~/Documents/Finanzielles/Einreichung Versicherung")))))
(setq stfl/org-roam-absolute (doom-path org-directory "roam/"))
(after! org-roam
  (setq! org-roam-capture-templates
         `(("d" "default" plain "%?"
            :target (file+head ,(doom-path stfl/org-roam-absolute "%<%Y%m%d%H%M%S>-${slug}.org")
                               "#+title: ${title}\n")
            :unnarrowed t))))

Capture Bills for Insurance Claims

(after! org
  (defun stfl/org-capture-versicherung-post ()
    (unless org-note-abort
      (mkdir (org-capture-get :directory) t)))

  (defun stfl/build-versicherung-dir (root date title)
    (let ((year (nth 5 (parse-time-string date))))
      (format "%s/%d/%s %s" root year date title)))

  (defun stfl/org-capture-template-versicherung ()
    (interactive)
    (let* ((date (org-read-date nil nil nil "Datum der Behandlung" nil nil t))
           (title (read-string "Title: "))
           (directory (stfl/build-versicherung-dir (org-capture-get :root) date title)))
      (org-capture-put :directory directory)
      (add-hook! 'org-capture-after-finalize-hook :local #'stfl/org-capture-versicherung-post)
      (format "* SVS [%s] %s
:PROPERTIES:
:CREATED:  %%U
:date:     [%s]
:betrag:   %%^{Betrag|0}
:svs:      nil
:generali: nil
:category: %%^{Kategorie|nil|Arzt|Alternativ|Internet|Psycho|Besonders|Apotheke|Vorsorge|Heilbehelfe|Brille|Transport}
:END:

[[file:%s]]

%%?" date title date directory)))
)

Archive

(after! org (setq org-archive-location (doom-path org-directory "archive/%s::datetree")))

org-checklist

org-checklist can be used to automatically reset the checkboxes in a recurring task

set the RESET_CHECK_BOXES property to t to reset the checklist on repeat

(after! org (require 'org-checklist))

org-habit

load org-habit because many of the functions in org-helpers.el require it…

(use-package! org-habit
  :after org-agenda
  :config
  (add-to-list 'org-modules 'org-habit)

  (setq org-habit-show-habits t
        org-habit-preceding-days 14
        org-habit-following-days 7
        ;; org-habit-graph-column 31 ;; Length of the habit graph
        ))

org-clock

(after! org-clock
  (setq! org-clock-rounding-minutes 15  ;; Org clock should clock in and out rounded to 5 minutes.
         org-time-stamp-rounding-minutes '(0 15)
         org-duration-format 'h:mm  ;; format hours and don't Xd (days)
         org-clock-report-include-clocking-task t  ;; include current task in the clocktable
         org-log-note-clock-out t
         org-agenda-clockreport-parameter-plist '(:link t :maxlevel 2 :stepskip0 t :fileskip0 t :hidefiles t :tags t)
         ))

Prompt to continue from the last clock-out time if the gap is

(after! org-clock
  (setq! org-clock-continuously nil))  ;; org-clock-continuously is handled by the advice

(after! org
  (defvar stfl/org-clock-continous-threshold 30)

  (defun stfl/org-read-date-time ()
    (let ((now (org-current-time org-clock-rounding-minutes t)))
      (org-read-date t t nil nil now (format-time-string "%H:%M" now))))

  (defun stfl/org-clock-in-at ()
    (interactive)
    (require 'org-clock)
    (let ((time (stfl/org-read-date-time))
          (org-clock-continuously (org-clocking-p)))

      (when (org-clocking-p)
        ;; Sanity check to avoid negative clock times -> best resolve manually
        (when (> 0 (time-subtract time org-clock-start-time))
          (error (format "Manually clocking in while another LATER clock is running! \"%s\" started at %s"
                         org-clock-heading (format-time-string (org-time-stamp-format 'with-time t) org-clock-start-time))))
        (org-clock-out nil nil time))

      (org-clock-in nil time)))

  (defun stfl/org-clock-out-at ()
    (interactive)
    (when (org-clocking-p) (org-clock-out nil nil (stfl/org-read-date-time))))

  (defun stfl/org-time-minutes-ago-rounded (time)
    (/ (org-time-convert-to-integer
        (time-subtract (org-current-time org-clock-rounding-minutes t) time))
       60))

  (defun stfl/org-time-minutes-ago (time)
    (/ (org-time-convert-to-integer
        (time-subtract (org-current-time) time))
       60))

  (defun stfl/org-time-format-ago (time)
    (format "%s (-%dm) (~%dm)"
            (format-time-string (org-time-stamp-format 'with-time t) time)
            (stfl/org-time-minutes-ago time)
            (stfl/org-time-minutes-ago-rounded time)))

  (defadvice! stfl/org-clock-continue? (orig-fn &rest args)
    "Prompt to continue on clock on clock out time if longer than `stfl/org-clock-continous-threshold`."
    :around #'org-clock-in
    (interactive "P")
    (let* ((start-time (cadr args))
           (org-clock-continuously
            (if start-time
                org-clock-continuously  ;; apply previous value
              (or (org-clocking-p)
                  (and org-clock-out-time
                       (or (< (stfl/org-time-minutes-ago org-clock-out-time) stfl/org-clock-continous-threshold)
                           (y-or-n-p (format "You stopped another clock at %s; start this one from then? "
                                             (stfl/org-time-format-ago org-clock-out-time)))))))))
      (apply orig-fn args)))

  (map! :map org-mode-map
        :localleader
        :prefix "c"
        :desc "clock IN at time" "I" #'stfl/org-clock-in-at
        :desc "clock OUT at time" "O" #'stfl/org-clock-out-at))

org-clock export to csv

(package! org-clock-csv)
(use-package org-clock-csv
  :after org
  :commands stfl/org-clock-export
  :config
  (defun stfl/org-clock-csv-row-fmt (plist)
    "Default row formatting function."
    (mapconcat #'identity
               (list (org-clock-csv--escape (plist-get plist ':task))
                     (org-clock-csv--escape (s-join org-clock-csv-headline-separator (plist-get plist ':parents)))
                     (org-clock-csv--escape (org-clock-csv--read-property plist "ARCHIVE_OLPATH"))
                     (org-clock-csv--escape (plist-get plist ':category))
                     (plist-get plist ':start)
                     (plist-get plist ':end)
                     (plist-get plist ':effort)
                     (plist-get plist ':ishabit)
                     (plist-get plist ':tags)
                     (org-clock-csv--read-property plist "ARCHIVE_ITAGS")
                     (org-clock-csv--read-property plist "AP"))
               ","))
  (setq! org-clock-csv-header "task,parents,archive_parents,category,start,end,effort,ishabit,tags,archive_tags,ap"
         org-clock-csv-row-fmt #'stfl/org-clock-csv-row-fmt)

  (setq stfl/org-clock-export-dir "~/work/invoice.typ/invoices")

  (defun stfl/org-clock-export (project)
    (interactive
     (list (completing-read "Select project: " agile-gtd-project-files)))
    (let* ((org-agenda-files (list (doom-path org-directory project)
                                   (doom-path org-directory "archive" project)))
           (filename (format "%s-org-clock-%s.csv" (format-time-string "%Y-%m") (file-name-base project)))
           (filepath (doom-path stfl/org-clock-export-dir filename)))
      (org-clock-csv-to-file filepath))))

(map! :map org-mode-map
    :leader
    :prefix "n"
    :desc "Export project clock entries" "E" #'stfl/org-clock-export)

(map! :map org-mode-map
    :localleader
    :prefix "c"
    :desc "Export project clock entries" "C" #'stfl/org-clock-export)

Task Dependencies (org-edna)

(package! org-edna)

Extensible Dependencies ’N’ Actions (EDNA) for Org Mode tasks

(use-package! org-edna
  :after org
  ;; :hook org-mode-hook  ;; load package after hook
  ;; :config (org-edna-mode)  ;; enable after load
  )

(add-hook! 'org-mode-hook #'org-edna-mode)

Helper commands to wire tasks into sequential dependency chains (defined in agile-gtd).

(map! :after org
      :map org-mode-map
      :localleader
      :prefix ("d" . "date/dateline/dependencies")
      :desc "next-sibling NEXT"          "n" #'agile-gtd-trigger-next-sibling
      :desc "trigger NEXT and block prev" "b" #'agile-gtd-chain-task)

Todo keywords

After much feedback and discussing with other users, I decided to simplify the keyword list to make it simple. Defining a project will now focus on the tag word :project: so that all child task are treated as part of the project.

KeywordDescription
TODO
PROJTask has actionable items defined and ready to be worked.
HOLDHas actionable items, but is on hold due to various reasons.
WAITWaiting for something
NEXTIs ready to be worked and should be worked on soon.
IDEAMight do, that but most likely drop it
DONETask is completed and closed.
KILLAbandoned or terminated.

Tracking TODO state changes

;; TODO keywords are configured in agile-gtd.
(custom-set-faces!
  `(agile-gtd-todo-cancel :foreground ,(doom-blend (doom-color 'red) (doom-color 'base5) 0.35) :inherit (bold org-done))
  `(agile-gtd-todo-idea :foreground ,(doom-darken (doom-color 'green) 0.4) :inherit (bold org-todo)))

Logging and Drawers

For the logging drawers, we like to keep our notes and clock history seperate from our properties drawer…

(after! org (setq org-log-state-notes-insert-after-drawers nil))

Next, we like to keep a history of our activity of a task so we track when changes occur, and we also keep our notes logged in their own drawer. Optionally you can also add the following in-buffer settings to override the org-log-into-drawer function. #+STARTUP: logdrawer or #+STARTUP: nologdrawer

(after! org
  (setq org-log-into-drawer t
        org-log-done 'time+note
        org-log-repeat 'time
        org-log-redeadline 'time
        org-log-reschedule 'time
        ))

Properties

(after! org
  (setq org-use-property-inheritance t ; We like to inherit properties from their parents
        org-catch-invisible-edits 'error ; Catch invisible edits
        org-track-ordered-property-with-tag t
        org-hierarchical-todo-statistics nil
        ))

Default Tags

(setq org-tag-alist '((:startgrouptag)
                      ("Context" . nil)
                      (:grouptags)
                      ;; ("@home" . ?h)
                      ;; ("@office". ?o)
                      ("@sarah" . ?s)
                      ("@lena" . ?l)
                      ;; ("@kg" . ?k)
                      ("@jg" . ?j)
                      ("@mfg" . ?m)
                      ;; ("@robert" . ?r)
                      ;; ("@baudock_meeting" . ?b)
                      ;; ("@PC" . ?p)
                      ;; ("@phone" . ?f)
                      (:endgrouptag)
                      ))

org-roam

Roam directory setup

(after! org-roam
  (setq! org-roam-directory org-directory
         org-roam-db-location (doom-path doom-local-dir "roam.db")
         org-roam-file-exclude-regexp "\\.org/\\(?:jira\\|\\.stversions\\)/"))

do not automatically open the roam side-pane

(after! org-roam
  (setq +org-roam-open-buffer-on-find-file nil))

customize the sections in the org-roam buffer

(after! org-roam-mode
  (add-to-list 'org-roam-mode-sections #'org-roam-unlinked-references-section t))
(after! org-roam
  (setq org-roam-dailies-capture-templates
        '(("d" "default"
           entry "* %?\n:PROPERTIES:\n:ID: %(org-id-new)\n:END:\n\n"
           :target (file+head "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n")))))

org-roam-ui

;; (package! websocket)
;; (package! org-roam-ui
;;   :recipe (:host github
;;            :repo "org-roam/org-roam-ui"
;;            :files ("*.el" "out")))
;; (use-package! websocket
;;     :after org-roam)

;; (use-package! org-roam-ui
;;     :after org-roam ;; or :after org
;; ;;         normally we'd recommend hooking orui after org-roam, but since org-roam does not have
;; ;;         a hookable mode anymore, you're advised to pick something yourself
;; ;;         if you don't care about startup time, use
;; ;;  :hook (after-init . org-roam-ui-mode)
;;     :config
;;     (setq org-roam-ui-sync-theme t
;;           org-roam-ui-follow t
;;           org-roam-ui-update-on-save t
;;           org-roam-ui-open-on-start nil))

org-gcal

(after! org-gcal
;; (use-package! org-gcal
  (setq org-gcal-client-id (get-auth-info "org-gcal-client-id" "ste.lendl@gmail.com")
        org-gcal-client-secret (get-auth-info "org-gcal-client-secret" "ste.lendl@gmail.com")
        org-gcal-fetch-file-alist
        `(("ste.lendl@gmail.com" . ,(doom-path org-directory "gcal/stefan.org"))
          ("vthesca8el8rcgto9dodd7k66c@group.calendar.google.com" . ,(doom-path org-directory "gcal/oskar.org")))
        org-gcal-token-file "~/.config/authinfo/org-gcal-token.gpg"
        org-gcal-down-days 180
        ;; org-gcal-auto-archive nil ;; workaround for "rx "**" range error" https://github.com/kidd/org-gcal.el/issues/17
        ))
(map!
 :after (org org-gcal)
 :map org-mode-map
 :leader
 (:prefix ("n" . "notes")
  (:prefix ("j" . "sync")
   :desc "sync Google Calendar" "g" #'org-gcal-sync)))

(map!
 :after (org org-gcal)
 :map org-mode-map
 :localleader
 :prefix ("C" . "Google Calendar")
   :desc "sync Google Calendar" "g" #'org-gcal-sync
   "S" #'org-gcal-sync-buffer
   "p" #'org-gcal-post-at-point
   "d" #'org-gcal-delete-at-point
   "f" #'org-gcal-fetch
   "F" #'org-gcal-fetch-buffer)

Drawing Diagrams with Mermaid

Org babel to generate mermaid diagrams from org src blocks

(package! ob-mermaid
  :disable t)
(use-package! ob-mermaid
  :after org
  :init
  (setq ob-mermaid-cli-path "/home/stefan/.yarn/bin/mmdc")
  :config
  (add-to-list 'org-babel-load-languages '(mermaid . t)))

org-babel

Auto :async if possible

https://tecosaur.github.io/emacs-config/config.html#babel

(add-transient-hook! #'org-babel-execute-src-block
  (require 'ob-async))

(defvar org-babel-auto-async-languages '()
  "Babel languages which should be executed asyncronously by default.")

(defadvice! org-babel-get-src-block-info-eager-async-a (orig-fn &optional light datum)
  "Eagarly add an :async parameter to the src information, unless it seems problematic.
This only acts o languages in `org-babel-auto-async-languages'.
Not added when either:
+ session is not \"none\"
+ :sync is set"
  :around #'org-babel-get-src-block-info
  (let ((result (funcall orig-fn light datum)))
    (when (and (string= "none" (cdr (assoc :session (caddr result))))
               (member (car result) org-babel-auto-async-languages)
               (not (assoc :async (caddr result))) ; don't duplicate
               (not (assoc :sync (caddr result))))
      (push '(:async) (caddr result)))
    result))

Individual startup visibility with :hidden

https://emacs.stackexchange.com/a/44923/30180

(after! org
  (defun individual-visibility-source-blocks ()
    "Fold some blocks in the current buffer with property :hidden"
    (interactive)
    (org-show-block-all)
    (org-block-map
     (lambda ()
       (let ((case-fold-search t))
         (when (and
                (save-excursion
                  (beginning-of-line 1)
                  (looking-at org-block-regexp))
                (cl-assoc
                 ':hidden
                 (cl-third
                  (org-babel-get-src-block-info))))
           (org-hide-block-toggle))))))

  (add-hook 'org-mode-hook #'individual-visibility-source-blocks))

Org Pandoc Import

https://github.com/tecosaur/org-pandoc-import

;; (package! org-pandoc-import
;;   :recipe (:host github
;;            :repo "tecosaur/org-pandoc-import"
;;            :files ("*.el" "filters" "preprocessors")))
;; (use-package! org-pandoc-import :after org)

Org Tree Slide (+present)

Don’t emphasize the heading -> it’s way too big

(after! org-tree-slide (setq org-tree-slide-heading-emphasis nil))

disable line numbers in presentations.

(after! org-tree-slide
  (add-hook 'org-tree-slide-play-hook #'doom-disable-line-numbers-h)
  (add-hook 'org-tree-slide-stop-hook #'doom-disable-line-numbers-h))

Starting org-tree-slide fails with an error. doomemacs/doomemacs#7058

(after! org-tree-slide
  (remove-hook 'org-tree-slide-play-hook #'+org-present-hide-blocks-h)
  (remove-hook 'org-tree-slide-stop-hook #'+org-present-hide-blocks-h))

Orgzly compatability format

(package! orgzly-formatter
  :recipe (:host github :repo "stfl/orgzly-formatter.el"))
(use-package orgzly-formatter
  :hook (org-mode . orgzly-formatter-mode))

orgzly-formatter already removes trailing whitespaces and keeps certain ones in specific cases.

(with-eval-after-load 'ws-butler
  (add-to-list 'ws-butler-global-exempt-modes 'org-mode))

ox-hugo

(package! ox-hugo)
(use-package! ox-hugo :after ox)

ox-zola

(package! ox-zola
  :recipe (:host github :repo "gicrisf/ox-zola"))
(use-package! ox-zola
  :after ox
  :config
  (require 'ox-hugo))

[#D] Org Agenda

Key bindings

(map! :after org-agenda
      :map org-agenda-mode-map
      :desc "Prioity up" "C-S-k" #'org-agenda-priority-up
      :desc "Prioity down" "C-S-j" #'org-agenda-priority-down

      :localleader
      "N" #'org-agenda-add-note
      :desc "Filter" "f" #'org-agenda-filter
      :desc "Follow" "F" #'org-agenda-follow-mode
      "o" #'org-agenda-set-property
      "s" #'org-toggle-sticky-agenda

      :prefix ("p" . "priorities")
      :desc "Prioity" "p" #'org-agenda-priority
      :desc "Prioity up" "u" #'org-agenda-priority-up
      :desc "Prioity down" "d" #'org-agenda-priority-down
      :desc "Someday/Maybe toggle" "s" #'agile-gtd-agenda-toggle-someday
      :desc "Add to Someday/Maybe" "S" #'agile-gtd-agenda-set-someday
      :desc "Tickler toggle" "t" #'agile-gtd-agenda-toggle-tickler
      :desc "Add to Tickler" "T" #'agile-gtd-agenda-set-tickler
      :desc "Remove Someday/Maybe" "r" #'agile-gtd-agenda-remove-someday

      :prefix ("v" . "View up to priority")
      "v" #'agile-gtd-agenda-show-priorities
      "l" #'agile-gtd-agenda-show-less-priorities
      "m" #'agile-gtd-agenda-show-more-priorities
      "r" #'agile-gtd-agenda-reset-show-priorities
      )

(map! :after org-ql
      :map org-ql-view-map
      "z" #'org-ql-view-dispatch)

Agenda options

;; (after! org
(setq!
       ;; org-agenda-dim-blocked-tasks t
       org-agenda-dim-blocked-tasks 'invisible
       org-agenda-use-time-grid t
       ;; org-agenda-hide-tags-regexp "\\w+"
       ;; org-agenda-compact-blocks t
       ;; org-agenda-block-separator ?\n
       org-agenda-block-separator ?-
       org-agenda-tags-column 0
       org-agenda-skip-scheduled-if-done t
       org-agenda-skip-unavailable-files t
       org-agenda-skip-deadline-if-done t
       org-agenda-skip-timestamp-if-done t
       org-agenda-window-setup 'current-window
       org-agenda-start-on-weekday nil
       org-agenda-span 'day
       org-agenda-start-day "-0d"
       org-deadline-warning-days 7
       org-agenda-show-future-repeats t
       org-agenda-skip-deadline-prewarning-if-scheduled t
       org-agenda-tags-todo-honor-ignore-options t
       org-agenda-skip-scheduled-delay-if-deadline t
       org-agenda-skip-scheduled-if-deadline-is-shown t
       org-agenda-skip-timestamp-if-deadline-is-shown t
       ;; org-agenda-todo-ignore-with-date nil
       ;; org-agenda-todo-ignore-deadlines nil
       ;; org-agenda-todo-ignore-timestamp nil
       org-agenda-todo-list-sublevels t
       org-agenda-include-deadlines t
       org-agenda-sticky t)
(after! org
  (setq org-enforce-todo-checkbox-dependencies nil
        org-enforce-todo-dependencies nil))

org super agenda

(package! org-super-agenda)
(after! (org-super-agenda evil-org-agenda)
  (setq org-super-agenda-header-map evil-org-agenda-mode-map))

Custom priority grouping

The priority grouping is generated dynamically by agile-gtd.

grouping based on my ancestor priorities

The ancestor-priority grouping is implemented in agile-gtd.

Grouping habits and Tickler in today agenda

This grouping is implemented in agile-gtd.

edit SOMEDAY entries

To mark entries (mainly PROJ) as not relevant at the moment I mark them with the tag SOMEDAY. If the enty has a SCHEDULED date assigned it’s considered a TICKLER entry. A TICKLER entry is not relevant right now but will be relevant at some point in the future. For the time beeing I want it to disapear from the todo backlog. On the scheduling date it will be added back into the system by removing the SOMEDAY tag and the schduling date.

Mark an agenda entry

These commands are provided by agile-gtd.

org-ql

(package! org-ql)

Agile GTD requires org-ql directly from the module.

Recurring Checkboxes org-checklist

(after! org-contrib
  (require 'org-checklist))

Module Settings

auth-sources and passwords

function to load a secret from an auth-source. letoh/creation-prompt.el

(defun get-auth-info (host user &optional port)
  (let ((info (nth 0 (auth-source-search
                      :host host
                      :user user
                      :port port
                      :require '(:user :secret)))))
    (if info
        (let ((secret (plist-get info :secret)))
          (if (functionp secret)
              (funcall secret)
            secret))
      nil)))

get-password

(defun get-password (&rest keys)
  (let ((result (apply #'auth-source-search keys)))
    (when result
      (funcall (plist-get (car result) :secret)))))
;; (setq! auth-sources 'password-store)

Define Word

;; (use-package! define-word
;;   :after org
;;   :config
;;   (map! :after org
;;         :map org-mode-map
;;         :leader
;;         :desc "Define word at point" "@" #'define-word-at-point))

Pandoc

(setq org-pandoc-options '((standalone . t) (self-contained . t)))

ansi colors in plaintext files

https://tecosaur.github.io/emacs-config/config.html#plaintext

(after! text-mode
  (add-hook! 'text-mode-hook
             ;; Apply ANSI color codes
             (with-silent-modifications
               (ansi-color-apply-on-region (point-min) (point-max)))))

vterm

disable vterm from straight because we installed it via nix

(package! vterm :disable t)
(after! vterm
  (setq! vterm-max-scrollback 200000
         ;; vterm-min-window-width 5000
         )) ;; do not wrap long lines per default

Allow to insert C-x. otherwise it’s not possible to leave nano if exidently opened in vterm.

Send some keys directly to vterm to trigger zsh functions!

(map!
 :after vterm
 :map vterm-mode-map
 "C-c C-x" #'vterm--self-insert
 :n "C-r" #'vterm--self-insert
 :n "C-j" #'vterm--self-insert
 :i "C-j" #'vterm--self-insert
 :i "TAB" #'vterm-send-tab
 :i "<tab>" #'vterm-send-tab)

For some reason, emacs-vterm tries to send C-j as return to the terminal instead of C-m. This breaks my zsh config, so I am forcing it to send C-m.

Send C-j directly as configured above which then triggers history-substring-search-down

(after! vterm
  (defun vterm-send-return ()
    "Send `C-m' to the libvterm."
    (interactive)
    (deactivate-mark)
    (when vterm--term
      (process-send-string vterm--process "\C-m"))))

use bash when opening a vterm in a tramp dir:

(after! vterm
  (setq! vterm-tramp-shells '(("docker" "/bin/sh")
                              ("ssh" "/bin/bash"))))

eat

(package! eat
  :recipe (:type git
           :host codeberg
           :repo "akib/emacs-eat"
           :files ("*.el" ("term" "term/*.el") "*.texi"
                   "*.ti" ("terminfo/e" "terminfo/e/*")
                   ("terminfo/65" "terminfo/65/*")
                   ("integration" "integration/*")
                   (:exclude ".dir-locals.el" "*-tests.el"))))
(use-package! eat
  :config
  ;; Enable eat with eshell
  (eat-eshell-mode)

  ;; Use semi-char mode by default for better interaction
  (setq! eat-term-scrollback-size 50000
         eat-enable-yank-to-terminal t
         eat-enable-kill-from-terminal t))

Typst

(package! typst-ts-mode
  :recipe (:type git :host codeberg
           :repo "meow_king/typst-ts-mode"
           :files (:defaults "*.el")))
(use-package! typst-ts-mode
  :mode ("\\.typ\\'" . typst-ts-mode)
  :config
  (setq! typst-ts-watch-options "--open"
         typst-ts-indent-offset 2
         typst-ts-enable-raw-blocks-highlight t)
  (map! :map typst-ts-mode-map
        "C-c C-c" #'typst-ts-tmenu
        :localleader
        :desc "Compile" "c" #'typst-ts-compile
        :desc "Watch" "w" #'typst-ts-watch-mode
        :desc "Menu" "m" #'typst-ts-tmenu)
  (add-hook! 'typst-ts-mode-hook #'lsp!))

(with-eval-after-load 'treesit
  (add-to-list 'treesit-language-source-alist
               '(typst "https://github.com/uben0/tree-sitter-typst")))
(after! eglot
  (add-to-list 'eglot-server-programs
               `(typst-ts-mode . ("tinymist"))))
(after! lsp-mode
  (add-to-list 'lsp-language-id-configuration '(typst-ts-mode . "typst") t)

  (lsp-register-client
   (make-lsp-client :new-connection (lsp-stdio-connection "tinymist")
                    :activation-fn (lsp-activate-on "typst")
                    :server-id 'tinymist)))
(after! org
  (add-to-list 'org-src-lang-modes '("typst" . typst-ts))

  ;; Set up babel support for Typst
  (org-babel-do-load-languages 'org-babel-load-languages '((typst . t)))

  ;; Configure babel execution for Typst
  (defun org-babel-execute:typst (body params)
    "Execute a block of Typst code with org-babel."
    (message "Executing Typst code block")
    (let* ((in-file (org-babel-temp-file "typst-" ".typ"))
           (out-file (or (cdr (assq :file params))
                         (org-babel-temp-file "typst-" ".pdf")))
           (result-params (cdr (assq :result-params params)))
           (cmdline (or (cdr (assq :cmdline params)) "")))
      (with-temp-file in-file
        (insert body))
      (org-babel-eval
       (format "typst compile %s %s %s" cmdline in-file out-file)
       "")
      (when (member "file" result-params)
        (org-babel-result-cond result-params
          out-file
          (format "[[file:%s]]" out-file))))))
(package! ox-typst)
(use-package! ox-typst :after ox)

pdf-tools

(package! pdf-tools :built-in 'prefer)

systemd units as conf-mode

(add-to-list 'auto-mode-alist '("\\.service\\'" . conf-space-mode))

ssh-deploy

(defvar stfl/upload-local-mappings nil
  "Global alist to store local to remote mappings (local-file . remote-path).
Each entry maps an absolute local file path to its corresponding remote path
for ssh-deploy functionality.")

(defun stfl/upload-unregister-all-remotes ()
  "Clear all ssh-deploy mappings and remove buffer-local variables.
Iterates through all stored mappings in stfl/upload-local-mappings and clears
ssh-deploy buffer-local variables for any open buffers, then clears the
global mapping list."
  (interactive)
  ;; For each mapping, find open buffers and clear their local variables
  (dolist (mapping stfl/upload-local-mappings)
    (let* ((local-file (car mapping))
           (buffer (get-file-buffer local-file)))
      (when buffer
        (message "Unregistering ssh-deploy mapping from %s" buffer)
        (with-current-buffer buffer
          (setq-local ssh-deploy-root-local nil
                      ssh-deploy-root-remote nil)))))
  ;; Clear the global alist
  (setq stfl/upload-local-mappings nil))

(defun stfl/upload-register-mapping (&optional remote-path)
  "Register or unregister ssh-deploy mapping for current buffer.
With C-u prefix, unregisters the current buffer's mapping and removes it
from the global stfl/upload-local-mappings list. Otherwise prompts for REMOTE-PATH
and registers the mapping, storing it in both buffer-local variables and the
global mapping list. Updates or replaces any existing mapping for the current file."
  (interactive (if current-prefix-arg
                   (list nil)  ; Don't prompt when unregistering
                 (list (expand-file-name (read-file-name "Remote path: ")))))
  (require 'ssh-deploy)
  (let ((local-file (expand-file-name (buffer-file-name))))
    (if current-prefix-arg
        (progn
          (message "Unregistering ssh-deploy for this buffer")
          (setq-local ssh-deploy-root-local nil
                      ssh-deploy-root-remote nil)
          ;; Remove mapping from global alist
          (setq stfl/upload-local-mappings
                (assoc-delete-all local-file stfl/upload-local-mappings)))
      (progn
        (setq-local ssh-deploy-root-local local-file
                    ssh-deploy-root-remote remote-path)
        (message "registered ssh-deploy for this buffer to %s" ssh-deploy-root-remote)
        ;; Add/update mapping in global alist
        (setq stfl/upload-local-mappings
              (cons (cons local-file remote-path)
                    (assoc-delete-all local-file stfl/upload-local-mappings)))))))
(after! ssh-deploy
  (setq! ssh-deploy-async 1))

(map! :map ssh-deploy-menu-map
    :leader
    :prefix "r"
    "l" #'stfl/upload-register-mapping
    "L" #'stfl/upload-unregister-all-remotes)

zoxide

(when (executable-find "zoxide")
  (with-eval-after-load 'dired
    (add-hook 'dired-mode-hook (lambda ()
                                 (call-process-shell-command
                                  (format "zoxide add %s"  dired-directory) nil 0))))

  (add-hook 'find-file-hook (lambda ()
                              (call-process-shell-command
                               (format "zoxide add %s"  (file-name-directory buffer-file-name))
                               nil 0)))


  (defun find-file-with-zoxide ()
    (interactive)
    (let ((target (consult--read
		   (process-lines "zoxide" "query" "-l")
		   :prompt "Zoxide: "
		   :require-match nil
		   :lookup #'consult--lookup-member
		   :category 'file
		   :sort t)))
      (if target
	  (let ((default-directory (concat target "/")))
	    (call-interactively 'find-file))
	(call-interactively 'find-file))))

  (map! )
  )

Coding

(after! flycheck
  (map! :map flycheck-mode-map
        :leader
        "c x" #'consult-flycheck))

ielm REPL

map ielm to SPC :

(map! :leader ":" #'ielm)

lsp-mode

I am not using lsp-mode anymore

(after! lsp-treemacs
  (lsp-treemacs-sync-mode 1))

Key Mappings

(map! :after lsp-mode
      :map lsp-mode-map
      :leader
      :prefix ("c" . "+code")
      :desc "Diagnostic for Workspace" "X" #'lsp-treemacs-errors-list)

Options

lsp-mode settings

Enable hinlay hints and make the font a little smaller than the rest

(after! lsp-mode
  (setq! lsp-inlay-hint-enable t
         lsp-headerline-breadcrumb-enable t
         lsp-ui-sideline-enable nil)
  )

emacs-lsp-booster

(when (executable-find "emacs-lsp-booster")
  (after! lsp-mode
    (setq! lsp-use-plists t)

    (defun lsp-booster--advice-final-command (old-fn cmd &optional test?)
      "Prepend emacs-lsp-booster command to lsp CMD."
      (let ((orig-result (funcall old-fn cmd test?)))
        (if (and (not test?)                             ;; for check lsp-server-present?
                 (not (file-remote-p default-directory)) ;; see lsp-resolve-final-command, it would add extra shell wrapper
                 lsp-use-plists
                 (not (functionp 'json-rpc-connection)))  ;; native json-rpc
            (progn
              (message "Using emacs-lsp-booster for %s!" orig-result)
              (append '("emacs-lsp-booster" "--disable-bytecode" "--") orig-result))
          orig-result)))
    (advice-add 'lsp-resolve-final-command :around #'lsp-booster--advice-final-command)))

lsp-bridge

(use-package! lsp-bridge
  :disabled
  :config
  (setq! lsp-bridge-user-langserver-dir (doom-path doom-user-dir "langserver")
         lsp-bridge-enable-inlay-hint t
         lsp-bridge-enable-hover-diagnostic t
         lsp-bridge-enable-signature-help nil
         lsp-bridge-enable-auto-format-code nil
         lsp-bridge-enable-org-babel t
         lsp-bridge-log-level 'default
         acm-enable-capf t
         acm-enable-org-roam t
         )

  (set-lookup-handlers! 'lsp-bridge-mode
    :definition #'lsp-bridge-peek
    ;; :definition #'lsp-bridge-find-def
    :references #'lsp-bridge-find-references
    :documentation #'lsp-bridge-popup-documentation
    :implementations #'lsp-bridge-find-impl
    :type-definition #'lsp-bridge-find-type-def)

  (map! :map lsp-bridge-peek-keymap
        :g "C-j" #'lsp-bridge-peek-list-next-line
        :g "C-k" #'lsp-bridge-peek-list-prev-line
        :g "C-S-j" #'lsp-bridge-peek-file-content-next-line
        :g "C-S-k" #'lsp-bridge-peek-file-content-prev-line
        :g "RET" #'lsp-bridge-peek-jump
        :g "C-SPC" #'lsp-bridge-peek-jump
        :g "ESC" #'lsp-bridge-peek-abort)

  (map! :map lsp-bridge-mode-map
        :leader
        :n "c r" #'lsp-bridge-rename
        :n "c a" #'lsp-bridge-code-action
        :n "c f" #'lsp-bridge-code-format
        )

  (map! :map acm-mode-map
        :i "C-j" #'acm-select-next
        :i "C-k" #'acm-select-prev)

  (acm-mode t)
  (global-lsp-bridge-mode)
  ;; (lsp-bridge-semantic-tokens-mode t)
  )

https://github.com/manateelazycat/lsp-bridge/blob/master/acm/acm.el#L233

Flyover

https://github.com/konrad1977/flyover

(package! flyover :recipe (:host github :repo "konrad1977/flyover"))
(use-package! flyover
  :disabled
  :after flycheck
  :config
  (setq! flyover-checkers '(flycheck)
         ;; flyover-levels '(error warning info)  ; Show all levels
         ;; flyover-levels '(error warning)
         flyover-levels '(error)

         flyover-use-theme-colors t ;; Use theme colors for error/warning/info faces
         flyover-background-lightness 35; Adjust background lightness (lower values = darker)
         ;; flyover-percent-darker 40 ;; Make icon background darker than foreground
         flyover-text-tint nil ; 'lighter ;; or 'darker or nil
         ;; flyover-text-tint-percent 50 ;; "Percentage to lighten or darken the text when tinting is enabled."
         flyover-debug nil ;; Enable debug messages
         ;; flyover-debounce-interval 0.2 ;; Time in seconds to wait before checking and displaying errors after a change

         ;; flyover-wrap-messages t ;; Enable wrapping of long error messages across multiple lines
         flyover-max-line-length 100 ;; Maximum length of each line when wrapping messages

         flyover-hide-checker-name t

         flyover-show-virtual-line t ;;; Show an arrow (or icon of your choice) before the error to highlight the error a bit more.
         ;; flyover-virtual-line-type 'straight-arrow

         flyover-line-position-offset 1

         flyover-show-at-eol t ;;; show at end of the line instead.
         flyover-hide-when-cursor-is-on-same-line t ;;; Hide overlay when cursor is at same line, good for show-at-eol.
         flyover-virtual-line-icon " ──► " ;;; default its nil
         )
  (add-hook 'flycheck-mode-hook #'flyover-mode)
)

Code Formatting

mapping doom’s +format/buffer and +format/region

(map! (:when (modulep! :editor format)
       :v "g Q" '+format/region
       :v "SPC =" '+format/region
       :leader
       :desc "Format Buffer" "=" #'+format/buffer
       (:prefix ("b" "+buffer")
        :desc "Format Buffer" "f" #'+format/buffer)))

PHP

(after! (lsp-mode php-mode)
  (setq lsp-intelephense-licence-key (get-auth-info "intelephense" "ste.lendl@gmail.com")
        lsp-intelephense-files-associations '["*.php" "*.phtml" "*.inc"]
        lsp-intelephense-files-exclude '["**update.php**" "**/js/**" "**/fonts/**" "**/gui/**" "**/upload/**"
                                         "**/.git/**" "**/.svn/**" "**/.hg/**" "**/CVS/**" "**/.DS_Store/**"
                                         "**/node_modules/**" "**/bower_components/**"
                                         "**/vendor/**/{Test,test,Tests,tests}/**"]
        lsp-auto-guess-root nil
        lsp-idle-delay 0.8))

Python

(after! lsp-bridge
  (setq! lsp-bridge-python-multi-lsp-server "basedpyright_ruff"))

poetry

Poetry needs to scan for a project whenever a new file is opened. Tracking via projectile speeds this up significantly.

(after! poetry (setq poetry-tracking-strategy 'projectile))

conda

automatically activate a conda environment if present in a project

(after! conda (conda-env-autoactivate-mode))

projectile does not recognize conda projects

(after! projectile
  (projectile-register-project-type 'python-conda '("environment.yml")
                                    :project-file "environment.yml"
                                    :compile "conda build"  ;; does not exist
                                    :test "conda run pytest"
                                    :test-dir "tests"
                                    :test-prefix "test_"
                                    :test-suffix"_test"))

numpydoc.el

;; (package! numpydoc)
;; (use-package! numpydoc
;;   :after python-mode
;;   :commands numpydoc-generate
;;   :config
;;   (map! :map python-mode-map
;;         :localleader
;;         :prefix ("d" . "docstring")
;;         :desc "Generate Docstring" "d" #'numpydoc-generate))

emacs ipython notebook (ein)

ein displays images in a separate window by default. Use inline images instead

(after! ein
  (setq! ein:output-area-inlined-images t
         ein:worksheet-warn-obsolesced-keybinding nil))

To enable this in the notebook, configure matplotlib to produce inline images.

%matplotlib inline

don’t kill the *ein: buffer with ESC which causes the buffer not to work properly

org babel integration for ein

(when (modulep! :tools ein)
  (after! org
    (require 'ob-ein)))
(set-popup-rule! "^\\*ein:" :ignore t :quit nil)

emacs-jupyter

header arguments to for jupyter-python to work with plotly

(after! org
  (setq org-babel-default-header-args:jupyter-python
        '((:results . "value")
          (:session . "jupyter")
          (:kernel . "python3")
          (:pandoc . "t")
          (:exports . "both")
          (:cache . "no")
          (:noweb . "no")
          (:hlines . "no")
          (:tangle . "no")
          (:eval . "never-export"))))

Debug Test at point workaround

debugging python tests at point do not work with the default configuration. might be merged upstream: emacs-lsp/dap-mode#590

(after! (python-mode dap-mode)
  (dap-register-debug-template "Python :: Run pytest (at point) -- Workaround"
                             (list :type "python-test-at-point  "
                                   :args ""
                                   :program nil
                                   :module "pytest"
                                   :request "launch"
                                   :name "Python :: Run pytest (at point)")))

Rust

(map! :mode rustic-mode
      :map rustic-mode-map
      :localleader
      :desc "rerun test" "t r" #'rustic-cargo-test-rerun)
(after! rustic
  (when (executable-find "cargo-nextest")
    (setq! rustic-cargo-test-runner 'nextest)))

Configure inlay type hints

This requires lsp-inlay-hint-enable as configured in LSP/Inlay Hints

(after! lsp-rust
  (setq! lsp-rust-analyzer-binding-mode-hints t
  ;;        lsp-rust-analyzer-display-chaining-hints t
  ;;        lsp-rust-analyzer-display-closure-return-type-hints t
         lsp-rust-analyzer-display-lifetime-elision-hints-enable "skip_trivial"
  ;;        lsp-rust-analyzer-display-parameter-hints t
  ;;        lsp-rust-analyzer-hide-named-constructor t
         lsp-rust-analyzer-max-inlay-hint-length 40  ;; otherwise some types can get way out of hand
         )
  )
(after! eglot
  (setq eglot-workspace-configuration
        (plist-put eglot-workspace-configuration
                   :rust-analyzer
                   '(:inlayHints (:maxLength 40)))))

configre dap-mode for rust

(after! (rust-mode dap-mode)
  (dap-register-debug-template "Rust::GDB Run Configuration"
                               (list :type "gdb"
                                     :request "launch"
                                     :name "GDB::Run"
                                     :gdbpath "rust-gdb"
                                     :target nil
                                     :cwd nil)))

Nix

configure alejandra, an alternative nix formatter

(set-formatter! 'alejandra '("alejandra" "--quiet") :modes '(nix-ts-mode))

select the formatter for nix

(setq-hook! 'nix-ts-mode-hook +format-with 'alejandra)

MQL

(add-to-list 'auto-mode-alist '("\\.mq[45h]\\'" . cpp-mode))

Gitlab-CI

;; (use-package! gitlab-ci-mode
;;   :mode ".gitlab-ci.yml"
;;   )

;; (use-package! gitlab-ci-mode-flycheck
;;   :after flycheck gitlab-ci-mode
;;   :init
;;   (gitlab-ci-mode-flycheck-enable))

Kubernetes

(package! kubernetes :disable t)
(package! kubernetes-evil :disable t)
(package! kubernetes-helm :disable t)
(package! k8s-mode :disable t)
(use-package! kubernetes
  :commands (kubernetes-overview))
(use-package! kubernetes-evil
  :after kubernetes)
(use-package! kubernetes-helm
  :commands kubernetes-helm-status)
(use-package! k8s-mode
  :after yaml-mode
  :hook (k8s-mode . yas-minor-mode))

SQL

(package! sql-indent)
(use-package! sql-indent
  :after sql-mode)

Database Interface (edbi)

roam:edbi setup

EDBI has some dependencies has some dependencies Installation Instructions

(package! edbi :disable t)
(package! edbi-minor-mode :disable t)
(use-package! edbi
  :commands 'edbi:open-db-viewer)
(use-package! edbi-minor-mode
  :after sql-mode
  :hook sql-mode-hook)
;; (add-hook 'sql-mode-hook 'edbi-minor-mode)

Practice coding with Exercism

Exercism is a platform for learning various programing languages by solving small exercises. The exercises can can be solved locally.

Local Setup Instructions

(package! exercism-mode
  :disable t
  :recipe (:host github
           :repo "timotheosh/exercism-mode"))
(use-package! exercism-mode
  :after projectile
  :if (executable-find "exercism")
  :commands exercism
  :config (exercism-mode +1)
  :custom (exercism-web-browser-function 'browse-url))

Javascript | Typescript

Jest Testing Framework

(package! jest :disable t)
(map! :after rjsx-mode
      :map rjsx-mode-map
      :localleader
      :prefix ("t" "test")
      "f" #'jest-file
      "t" #'jest-function
      "k" #'jest-file-dwim
      "m" #'jest-repeat
      "p" #'jest-popup)

JSON

The :lang json module handles .jsonjson-ts-mode via tree-sitter. json-ts-mode natively supports // and /* */ comments, so .jsonc files can use it too.

(add-to-list 'auto-mode-alist '("\\.jsonc\\'" . json-ts-mode))

Perl

always use the better cperl-mode instead of perl-mode. This will be the default in Emacs 30 (if I remember correctly)

(add-to-list 'major-mode-remap-alist '(perl-mode . cperl-mode))

Logview

(package! logview :disable t)
(use-package! logview
  :commands logview-mode
  :config (setq truncate-lines t)
  (map! :map logview-mode-map
        "j" #'logview-next-entry
        "k" #'logview-previous-entry))

The default keymap binds directly to f t T a A ... switch to evil-emacs-mode C-z to use the keybindings.

Spell- Grammer Checking

LTEX

(package! lsp-ltex :disable t)

manually add adoc-mode to the list.. I don’t like that…

;; (add-to-list 'lsp-ltex-active-modes 'adoc-mode t)
(setq lsp-ltex-active-modes '(text-mode
                              bibtex-mode
                              context-mode
                              latex-mode
                              markdown-mode
                              org-mode
                              rst-mode
                              adoc-mode))
(use-package! lsp-ltex
  :after lsp-ltex-active-modes
  :hook (adoc-mode . (lambda ()
                       (require 'lsp-ltex)
                       (lsp-deferred)))  ; or lsp-deferred
  :init
  (setq lsp-ltex-server-store-path "~/.nix-profile/bin/ltex-ls"
        lsp-ltex-version "16.0.0"
        lsp-ltex-mother-tongue "de-AT"
        lsp-ltex-user-rules-path (doom-path doom-user-dir "lsp-ltex")))

ispell

(after! ispell
  (setq! ispell-personal-dictionary (expand-file-name "ispell/" doom-user-dir)))

Proxmox development

tangle is disabled for the entire section and is only kept as a reference!

AsciiDoc (adoc)

;; bbatsov/adoc-mode
(package! adoc-mode)

TODO adoc-mode requires org due to inhereting the theme

(use-package! adoc-mode
  :defer t
  :config
  (map! :map adoc-mode-map
        :localleader
        :desc "consult headers in this file" "." #'consult-imenu
        :desc "consult headers in project" "/" #'consult-imenu-multi
        "p" #'treemacs-find-tag)

  (custom-set-faces!
    '(adoc-code-face :inherit org-block)
    '(adoc-complex-replacement-face :inherit org-code :weight bold)
    '(adoc-meta-face :inherit org-meta-line)
    '(adoc-typewriter-face :inherit org-code)
    '(adoc-verbatim-face :inherit org-verbatim)
    '(adoc-internal-reference-face :inherit org-link)
    '(adoc-reference-face :inherit org-link)
    `(adoc-emphasis-face :foreground ,(doom-lighten (doom-color 'green) 0.2) :slant italic)
    '(adoc-bold-face :weight bold)
    `(adoc-command-face :foreground ,(doom-color 'base1) :background ,(doom-color 'base6))
    '(adoc-warning-face :inherit org-warning))
  )
(after! lsp-mode
  (add-to-list 'lsp-language-id-configuration '(adoc-mode . "org") t))

Magit Default Sign-Off commits

(after! magit
  (setq transient-values  '((magit-commit "--signoff"))))

Javascript indentation style

This is still broken…

(setq-hook! 'rjsx-mode-hook
  indent-tabs-mode t
  tab-width 8)

(setq-hook! 'js-mode-hook
  indent-tabs-mode t
  tab-width 8)

(setq-hook! 'js2-mode-hook
  indent-tabs-mode t
  tab-width 8)
(after! js
  (setq js-indent-level 4
        js-jsx-indent-level 4
        tab-width 8))

Perl intendation style

(setq! cperl-indent-level 4
       cperl-close-paren-offset -4
       cperl-continued-statement-offset 4
       cperl-indent-parens-as-block t)

Proxmox specific Perl indentation style using a tab where 8 spaces would be used.

(setq-hook! 'cperl-mode-hook
  tab-width 8
  indent-tabs-mode t)

notmuch

(after! notmuch
  (setq +notmuch-sync-backend 'mbsync
        +notmuch-mail-folder "~/Mail"
        notmuch-draft-folder "proxmox/Entw&APw-rfe"
        notmuch-fcc-dirs "proxmox/Sent"
        notmuch-mua-cite-function 'message-cite-original-without-signature
        notmuch-mua-compose-in 'current-window
        notmuch-show-logo nil
        notmuch-hello-indent 0  ;; do not indent because it works better with evil navigation
        notmuch-tag-formats '(("unread" (propertize tag 'face 'notmuch-tag-unread)))
        notmuch-saved-searches
        '((:key "i" :name "󰇮 inbox"   :query "tag:inbox and not tag:archive")
          (:key "f" :name " flagged" :query "tag:flagged")
          (:key "m" :name "󰇮 my PRs"  :query "tag:my-pr and not tag:archive and not tag:killed and not tag:deleted and not tag:inbox")
          (:key "w" :name "󰇮 watch"   :query "tag:watch and not tag:my-pr and not tag:archive and not tag:killed and not tag:deleted and not tag:inbox")
          (:key "t" :name "󰇮 team"    :query "tag:lists/team and not tag:archive and not tag:inbox")
          (:key "b" :name " My Bugs" :query "tag:bugs and tag:to-me and not tag:archive and not tag:inbox")
          (:key "s" :name " support (new)" :query "tag:support-new and not tag:archive and not tag:killed")
          (:key "r" :name " review"  :query "tag:review and not tag:archive and not tag:killed and not tag:inbox")
          (:key "d" :name " drafts"  :query "tag:draft and not tag:archive and not tag:deleted")
          ;; (:key ">" :name "󰗕 sent"    :query "tag:sent and not tag:archive")
          (:key "M" :name " my PRs" :query "tag:my-pr and not tag:killed and not tag:deleted and not tag:inbox")
          (:key "W" :name " watch" :query "tag:watch and not tag:killed and not tag:deleted and not tag:inbox")
          (:key "B" :name " Bugzilla" :query "tag:bugs and not tag:archive and not tag:inbox")
          (:key "S" :name " support" :query "tag:support and not tag:archive and not tag:killed and not tag:inbox")
          (:key "P" :name " pkgs"    :query "tag:lists/pkgs and not tag:archive and not tag:inbox"))
        notmuch-archive-tags '("+archive" "-inbox" "-unread")
        +notmuch-spam-tags '("+spam" "-inbox" "-unread")
        +notmuch-delete-tags '("+trash" "-inbox" "-unread")

        stfl/notmuch-unwatch-tags (append notmuch-archive-tags '("-my-pr" "-watch" "-review"))
        stfl/notmuch-kill-tags (cons "+killed" stfl/notmuch-unwatch-tags)

        message-hidden-headers nil  ;; don't hide any headers to verify In-reply-to and Reference headers
        notmuch-mua-hidden-headers nil

        message-sendmail-f-is-evil 't
        message-sendmail-extra-arguments '("--read-envelope-from")
        message-send-mail-function 'message-send-mail-with-sendmail
        sendmail-program "msmtp")
  (add-to-list '+word-wrap-disabled-modes 'notmuch-show-mode)
  (add-hook! 'notmuch-hello-mode-hook #'read-only-mode)
  (defun +notmuch-get-sync-command ()
    "mbsync -a && notmuch new && afew -n -t")

  (custom-set-faces!
    '(notmuch-message-summary-face      :foreground "#848d94")  ;; between dooms base6 and base7
    `(notmuch-wash-cited-text           :foreground ,(doom-color 'base6))
    `(notmuch-search-subject            :foreground ,(doom-darken (doom-color 'fg) 0.05))
    '(notmuch-search-unread-face        :weight bold :slant italic)
    `(notmuch-tree-match-tree-face      :foreground              ,(doom-color 'yellow))
    `(notmuch-tree-no-match-tree-face   :foreground              ,(doom-color 'base5))
    `(notmuch-tree-no-match-author-face :foreground ,(doom-darken (doom-color 'blue)    0.3))
    `(notmuch-tree-no-match-date-face   :foreground ,(doom-darken (doom-color 'numbers) 0.3))
    `(notmuch-tree-no-match-tag-face    :foreground ,(doom-darken (doom-color 'yellow)  0.4)))
  )

Additional functions to apply tags according to my workflow

(after! notmuch
  (defun stfl/notmuch-search-unwatch-thread (&optional unarchive beg end)
    (interactive (cons current-prefix-arg (notmuch-interactive-region)))
    (let ((notmuch-archive-tags stfl/notmuch-unwatch-tags))
      (notmuch-search-archive-thread unarchive beg end)))

  (defun stfl/notmuch-search-kill-thread (&optional unarchive beg end)
    (interactive (cons current-prefix-arg (notmuch-interactive-region)))
    (let ((notmuch-archive-tags stfl/notmuch-kill-tags))
      (notmuch-search-archive-thread unarchive beg end)))
  )

Use the (almost) default keybindings in evil normal-mode

(map! :after notmuch
      :map notmuch-common-keymap
      :n "?" #'notmuch-help
      :map notmuch-show-mode-map
      ;; :g "<mouse-1>" #'notmuch-show-toggle-message
      ;; :g "<mouse-2>" #'notmuch-show-toggle-message
      ;; :desc "toggle show message" :n "<tab>" #'notmuch-show-toggle-message
      ;; :desc "toggle show message" :n "C-<tab>" #'notmuch-show-open-or-close-all
      :g "C-c C-e" #'notmuch-show-resume-message
      :n "ge" #'notmuch-show-resume-message
      ;; :n "A" '(λ! (notmuch-search-tag-all "-archive -my-pr -watch"))  ;; TODO need to tag ENTIRE thread oterhwise it will be tagged again with afew
      :map notmuch-tree-mode-map
      :g "C-c C-e" #'notmuch-tree-resume-message
      :n "ge" #'notmuch-tree-resume-message
      :n "A" (λ! (notmuch-tree-tag-thread stfl/notmuch-unwatch-tags))
      :n "K" (λ! (notmuch-tree-tag-thread stfl/notmuch-kill-tags))
      :map notmuch-search-mode-map
      :n "A" #'stfl/notmuch-search-unwatch-thread
      :n "K" #'stfl/notmuch-search-kill-thread
      ;; :map notmuch-message-mode-map
      ;; :n "SPC f s" #'notmuch-draft-save
      )
(after! notmuch
  (defun stfl/notmuch-hello-update-background ()
    "Update notmuch-hello buffer. If we are in another frame, allow switch to it so it will be formatted correctly."
    (let ((no-display (eq (selected-frame)
                          (window-frame (display-buffer "*notmuch-hello*")))))
      (notmuch-hello no-display)))

  (run-with-idle-timer 60 t #'stfl/notmuch-hello-update-background))

ssh-config-mode

(package! ssh-config-mode)
(use-package! ssh-config-mode :defer t)

BitBake

(package! bitbake-ts-mode)
(with-eval-after-load 'treesit
  (add-to-list 'treesit-language-source-alist
               '(bitbake "https://github.com/tree-sitter-grammars/tree-sitter-bitbake")))
(use-package bitbake-ts-mode
  :config
  (add-to-list 'auto-mode-alist '("\\.inc$" . bitbake-ts-mode))
  (add-to-list 'auto-mode-alist '("\\.bbclass" . bitbake-ts-mode)))

Setup custom bitbake language server for bitbake-ts-mode via lsp-bridge.

(after! lsp-bridge
  (add-to-list 'lsp-bridge-single-lang-server-mode-list
               ;; '(bitbake-ts-mode . "bitbake-language-server")
               '(bitbake-ts-mode . "language-server-bitbake"))
  (add-to-list 'lsp-bridge-default-mode-hooks 'bitbake-ts-mode-hook t))

lsp-bridge requires a json config file to configure the language server. NOTE: the :tangle "langserver/<server>.json" needs to match the alist above (<mode> . <server>)

{
    "name": "bitbake-language-server",
    "languageId": "bitbake",
    "command": [ "bitbake-language-server" ],
    "settings": {}
}

Alternative bitbake lsp from the vscode-bitbake project.

{
    "name": "language-server-bitbake",
    "languageId": "bitbake",
    "command": [ "/home/stefan/node_modules/language-server-bitbake/out/server.js", "--stdio" ],
    "settings": {}
}

“command”: [ “language-server-bitbake”, “–stdio” ],

Meson build system

(package! meson-mode :disable t)
(use-package! meson-mode
  :config (add-hook! 'meson-mode-hook #'company-mode))

C/C++

(after! projectile
  (add-to-list 'projectile-globally-ignored-directories ".ccls-cache"))
(after! lsp-bridge
  (setq! lsp-bridge-c-lsp-server "ccls"))
(defun run-ctest (arg)
  (interactive "P")
  (let ((projectile-project-test-cmd "cmake --build build && ctest --test-dir build --output-on-failure --rerun-failed"))
    (projectile-test-project arg)))


(map! :mode c++-mode
      :map c++-mode-map
      :localleader
      :prefix ("t" "test")
      :n "t" #'run-ctest
      ;; :n "t" #'gtest-run-at-point
      ;; :n "T" #'gtest-run
      ;; :n "l" #'gtest-list
      )

Turbo Log

(package! turbo-log
  :recipe (:host github
           :repo "artawower/turbo-log"))
(use-package! turbo-log
  :after prog-mode
  :config
  (map! :leader
        "l l" #'turbo-log-print
        "l i" #'turbo-log-print-immediately
        "l h" #'turbo-log-comment-all-logs
        "l s" #'turbo-log-uncomment-all-logs
        "l [" #'turbo-log-paste-as-logger
        "l ]" #'turbo-log-paste-as-logger-immediately
        "l d" #'turbo-log-delete-all-logs)
  (setq! turbo-log-msg-format-template "\"🚀: %s\""
         turbo-log-allow-insert-without-treesit-p t))

just

(package! just-mode)
(use-package! just-mode
  :defer t)

Collaboration and VCS

Directory tree diff - ztree

(package! ztree :disable t)
(use-package! ztree)

Magit

Enable granular diff-highlights for all hunks

By default, changes are highlighted line-wise for all but the selected hunk. This has performance reasons. You can enable character-wise highlights for all visible hunks with:

(after! magit
  (setq magit-diff-refine-hunk 'all))

Forge

display more columns in forge list topic

(after! forge (setq forge-topic-list-columns
                    '(("#" 5 t (:right-align t) number nil)
                      ("Title" 60 t nil title  nil)
                      ("State" 6 t nil state nil)
                      ("Marks" 8 t nil marks nil)
                      ("Labels" 8 t nil labels nil)
                      ("Assignees" 10 t nil assignees nil)
                      ("Updated" 10 t nill updated nil))))

magit-todos

(package! magit-todos)

activate magit-todo to display the TODOs section in magit buffer

(use-package! magit-todos
  :after magit
  :config
  (setq! magit-todos-exclude-globs '(".git/" "node_modules/"))
  (magit-todos-mode 1))

E-Mail

mailscripts.el

https://github.com/spwhitton/mailscripts/blob/master/mailscripts.el

The original purpose of this package was to make it easy to use the small mail-handling utilities shipped in Debian’s ‘mailscripts’ package from within Emacs. It now also contains some additional, thematically-related utilities which don’t invoke any of those scripts.

Entry points you might like to look at if you’re new to this package: mailscripts-prepare-patch, notmuch-slurp-debbug, notmuch-extract-{thread,message}-patches{,-to-project}.

;; (package! mailscripts.el
;;   :recipe (:host github :repo "spwhitton/mailscripts" :files ("mailscripts.el")))

mu4e

;; (set-email-account! "gmail"
;;   '((mu4e-sent-folder       . "/gmail/[Google Mail]/Gesendet")
;;     (mu4e-drafts-folder     . "/gmail/[Google Mail]/Entw&APw-rfe")
;;     (mu4e-trash-folder      . "/gmail/[Google Mail]/Trash")
;;     (mu4e-refile-folder     . "/gmail/[Google Mail]/Alle Nachrichten")
;;     (smtpmail-smtp-user     . "ste.lendl@gmail.com")
;;     ;; (+mu4e-personal-addresses . "ste.lendl@gmail.com")
;;     ;; (mu4e-compose-signature . "---\nStefan Lendl")
;;     )
;;   t)
;; (set-email-account! "pulswerk"
;;   '((mu4e-sent-folder       . "/pulswerk/Sent Items")
;;     (mu4e-drafts-folder     . "/pulswerk/Drafts")
;;     (mu4e-trash-folder      . "/pulswerk/Deleted Items")
;;     (mu4e-refile-folder     . "/pulswerk/Archive")
;;     (smtpmail-smtp-user     . "lendl@pulswerk.at")
;;     ;; (+mu4e-personal-addresses . "lendl@pulswerk.at")
;;     ;; (mu4e-compose-signature . "---\nStefan Lendl")
;;     )
;;   t)
(after! mu4e
  ;; (setq +mu4e-gmail-accounts '(("ste.lendl@gmail.com" . "/gmail")))
  (setq mu4e-context-policy 'ask-if-none
        mu4e-compose-context-policy 'always-ask)

  (setq mu4e-maildir-shortcuts
    '((:key ?g :maildir "/gmail/Inbox"   )
      (:key ?p :maildir "/pulswerk/INBOX")
      (:key ?u :maildir "/gmail/Categories/Updates")
      (:key ?j :maildir "/pulswerk/Jira"  )
      (:key ?l :maildir "/pulswerk/Gitlab" :hide t)
      ))

  (setq mu4e-bookmarks
        '(
          (:key ?i :name "Inboxes" :query "not flag:trashed and (m:/gmail/Inbox or m:/pulswerk/INBOX)")
          (:key ?u :name "Unread messages"
           :query
           "flag:unread and not flag:trashed and (m:/gmail/Inbox or m:/gmail/Categories/* or m:/pulswerk/INBOX or m:\"/pulswerk/Pulswerk Alle\" or m:/pulswerk/Jira or m:/pulswerk/Gitlab)")
          (:key ?p :name "pulswerk Relevant Unread" :query "flag:unread not flag:trashed and (m:/pulswerk/INBOX or m:\"/pulswerk/Pulswerk Alle\" or m:/pulswerk/Jira or m:/pulswerk/Gitlab)")
          (:key ?g :name "gmail Relevant Unread" :query "flag:unread not flag:trashed and (m:/gmail/Inbox or m:/gmail/Categories/*)")
          ;; (:key ?t :name "Today's messages" :query "date:today..now" )
          ;; (:key ?y :name "Yesterday's messages" :query "date:2d..1d")
          ;; (:key ?7 :name "Last 7 days" :query "date:7d..now" :hide-unread t)
          ;; ;; (:name "Messages with images" :query "mime:image/*" :key 112)
          ;; (:key ?f :name "Flagged messages" :query "flag:flagged")
          ;; (:key ?g :name "Gmail Inbox" :query "maildir:/gmail/Inbox and not flag:trashed")
          ))
  )

set up the query for mu4e-alert to also limit the search range

(after! mu4e-alert
  (setq mu4e-alert-interesting-mail-query
           "flag:unread and not flag:trashed and (m:/gmail/Inbox or m:/gmail/Categories/Updates or m:/pulswerk/INBOX or m:\"/pulswerk/Pulswerk Alle\" or m:/pulswerk/Jira or m:/pulswerk/Gitlab)"))
(after! mu4e
  (setq mu4e-headers-fields
        '((:flags . 6)
          (:account-stripe . 2)
          (:from-or-to . 25)
          (:folder . 10)
          (:recipnum . 2)
          (:subject . 80)
          (:human-date . 8))
        +mu4e-min-header-frame-width 142
        mu4e-headers-date-format "%d/%m/%y"
        mu4e-headers-time-format "⧖ %H:%M"
        mu4e-headers-results-limit 1000
        mu4e-index-cleanup t)

  (defvar +mu4e-header--folder-colors nil)
  (appendq! mu4e-header-info-custom
            '((:folder .
               (:name "Folder" :shortname "Folder" :help "Lowest level folder" :function
                (lambda (msg)
                  (+mu4e-colorize-str
                   (replace-regexp-in-string "\\`.*/" "" (mu4e-message-field msg :maildir))
                   '+mu4e-header--folder-colors)))))))

Sending Mail

(after! mu4e
  (setq sendmail-program "/usr/bin/msmtp"
        send-mail-function #'smtpmail-send-it
        message-sendmail-f-is-evil t
        message-sendmail-extra-arguments '("--read-envelope-from") ; , "--read-recipients")
        message-send-mail-function #'message-send-mail-with-sendmail))

mu4e-views

requies emacs compiled with xwidgets

it can still use the browser view. select via mu4e-views-mu4e-select-view-msg-method

;; (use-package! mu4e-views
;;   :after mu4e
;;   )

org-msg

(setq +org-msg-accent-color "#1a5fb4"
      org-msg-greeting-fmt "\nHi %s,\n\n"
      org-msg-signature "\n\n#+begin_signature\n*MfG Stefan Lendl*\n#+end_signature")

(map! :map org-msg-edit-mode-map
      :after org-msg
      :n "G" #'org-msg-goto-body)

ediff

forcing text comparison even if diff thinks files are binary

(after! ediff
  (setq ediff-diff-options "--text"
        ediff-diff3-options "--text"
        ediff-toggle-skip-similar t
        ediff-diff-options "-w"
        ;; ediff-window-setup-function 'ediff-setup-windows-plain
        ediff-split-window-function 'split-window-horizontally
        ediff-floating-control-frame t
        ))

diffview

Render a unified diff (top/bottom) in an easy-to-comprehend side-by-side format. This comes in handy for reading patches from mailing lists (or from whencever you might acquire them).

(package! diffview :disable t)
(use-package! diffview
  :commands diffview-current
  :config
  (map!
   :after notmuch
   :localleader "d" #'diffview-current))

blamer.el

https://github.com/Artawower/blamer.el git blame lenses

(package! blamer)
(use-package! blamer
  :commands global-blamer-mode
  :init (map! :leader "t B" #'global-blamer-mode)
  :config
  (map! :leader "g i" #'blamer-show-posframe-commit-info)
  (setq! blamer-idle-time 0.3
         blamer-max-commit-message-length 80
         ;; blamer-max-lines 100
         blamer-type 'visual
         ;; blamer-type 'posframe-popup
         ;; blamer-type 'overlay-popup
         blamer-min-offset 40)

  ;; (custom-set-faces!
  ;;   `(blamer-face :inherit font-lock-comment-face
  ;;     :slant italic
  ;;     :font "JetBrains Mono"
  ;;     ;; :height 0.9
  ;;     :background unspecified
  ;;     ;; :weight semi-light
  ;;     ;; :foreground ,(doom-color 'base5)
  ;;     ))

  (add-hook! org-mode-hook (λ! (blamer-mode 0))))

AI

(map!
      ;; "C-c a" #'aidermacs-transient-menu
      :leader
      (:prefix ("j" . "AI")
       ;; "m" #'gptel-menu
       ;; "j" #'gptel
       ;; "C-g" #'gptel-abort
       ;; "C-c" #'gptel-abort
       ;; :desc "Toggle context" "C" #'gptel-add
       ;; "s" #'gptel-system-prompt
       ;; "w" #'gptel-rewrite-menu
       ;; "t" #'gptel-org-set-topic
       ;; "P" #'gptel-org-set-properties

       ;; "a" #'aidermacs-transient-menu
       ;; "a" #'aider-transient-menu

       "o" #'claude-code-ide-menu

       (:prefix ("c" . "Copilot Chat")
        ;; "" #'copilot-chat-reset  ;; reset everything including history, buffers and frontend.
        "c" #'copilot-chat-display  ;; display copilot chat buffers.
        "s" #'copilot-chat-explain-symbol-at-line  ;; ask Copilot to explain symbol under point.
        "e" #'copilot-chat-explain  ;; ask copilot to explain selected code.
        "r" #'copilot-chat-review  ;; ask copilot to review selected code.
        "d" #'copilot-chat-doc  ;; ask copilot to document selected code.
        "f" #'copilot-chat-fix  ;; ask copilot to fix selected code.
        "o" #'copilot-chat-optimize  ;; ask copilot to optimize selected code.
        "t" #'copilot-chat-test  ;; ask copilot to write tests for selected code.
        ;; :n "" #'copilot-chat-custom-prompt-selection  ;; ask for a prompt in minibuffer and pastes selection after it before sending it to copilot.
        "b" #'copilot-chat-add-current-buffer  ;; add current buffer to copilot chat. Its content will be sent with every request.
        "B" #'copilot-chat-del-current-buffer  ;; remove current buffer.
        "l" #'copilot-chat-list  ;; open buffer list.
        ;; "" #'copilot-chat-prompt-history-previous  ;; insert previous prompt from history in prompt buffer.
        ;; "" #'copilot-chat-prompt-history-next  ;; insert next prompt from history in prompt buffer.
        "a" #'copilot-chat-ask-and-insert  ;; ask for a custom prompt and write answer in current buffer at point.
        "m" #'copilot-chat-insert-commit-message  ;; Insert in the current buffer a copilot generated commit message.
        )))
(defun stfl/setup-api-keys ()
  (interactive)
  (message "Setting up API keys")
  (setenv "OPENAI_API_KEY" (password-store-get "API/OpenAI-emacs"))
  (setenv "ANTHROPIC_API_KEY" (password-store-get "API/Claude-emacs"))
  (setenv "GEMINI_API_KEY" (password-store-get "API/Gemini-emacs"))
  (setenv "PERPLEXITYAI_API_KEY" (password-store-get "API/Perplexity-emacs-pro-ste.lendl"))
  (setenv "OPENROUTER_API_KEY" (password-store-get "API/Openrouter-emacs")))

Copilot

(package! copilot
  :recipe (:host github
           :repo "zerolfx/copilot.el"
           :files ("*.el" "dist")))
(use-package! copilot
  :hook (prog-mode . copilot-mode)
  :after prog-mode
  :config
  ;; Define the custom function that either accepts the completion or does the default behavior
  (defun +copilot-tab-or-default ()
    (interactive)
    (if (and (bound-and-true-p copilot-mode)
             ;; Add any other conditions to check for active copilot suggestions if necessary
             )
        (copilot-accept-completion)
      (evil-insert 1))) ; Default action to insert a tab. Adjust as needed.

  ;; Bind the custom function to <tab> in Evil's insert state
  ;; (evil-define-key 'insert 'global (kbd "<tab>") #'+copilot-tab-or-default)

  (map! :map copilot-completion-map
        "<tab>" #'+copilot-tab-or-default
        "TAB" #'+copilot-tab-or-default
        ;; :i "C-TAB" #'copilot-accept-completion-by-word
        ;; :i "C-<tab>" #'copilot-accept-completion-by-word
        "C-S-n" #'copilot-next-completion
        ;; :i "C-<tab>" #'copilot-next-completion
        "C-S-p" #'copilot-previouse-completion
        ;; :i "C-<iso-lefttab>" #'copilot-previouse-completion
        )

  (add-to-list 'copilot-indentation-alist '(org-mode 2))

  (setq! copilot-indent-offset-warning-disable t
         copilot-max-char-warning-disable t)

  (setq copilot-lsp-settings '(:github (:copilot (:selectedCompletionModel "gpt-41-copilot"))))
  )

Copilot Chat

(package! copilot-chat
  :recipe (:host github
           :repo "chep/copilot-chat.el"
           :files ("*.el")))
(use-package copilot-chat
  :after org
  :commands (copilot-chat-insert-commit-message copilot-chat-fix copilot-chat-doc)
  :config (setq! copilot-chat-model "claude-3.7-sonnet"
                 copilot-chat-frontend 'org)

  ;; (add-hook 'git-commit-setup-hook 'copilot-chat-insert-commit-message)
  ;; Or call manually (copilot-chat-insert-commit-message) when in the commit message buffer.
  )

KILL Codeium

A free alternative to Github Copilot

(package! codeium
  :recipe (:host github
           :repo "Exafunction/codeium.el")
  :disable t)
(use-package! codeium
  :defer t  ;; TODO to start it, manually call codeium-init

  ;; if you use straight
  ;; :straight '(:type git :host github :repo "Exafunction/codeium.el")
  ;; otherwise, make sure that the codeium.el file is on load-path

  :init
  ;; use globally
  (add-to-list 'completion-at-point-functions #'codeium-completion-at-point)
  ;; (add-to-list 'company-frontends #'company-preview-frontend)
  (setq company-minimum-prefix-length 0)

  ;; or on a hook
  ;; (add-hook 'python-mode-hook
  ;;     (lambda ()
  ;;         (setq-local completion-at-point-functions '(codeium-completion-at-point))))

  ;; if you want multiple completion backends, use cape (https://github.com/minad/cape):
  ;; (add-hook 'python-mode-hook
  ;;     (lambda ()
  ;;         (setq-local completion-at-point-functions
  ;;             (list (cape-super-capf #'codeium-completion-at-point #'lsp-completion-at-point)))))

  ;; TODO for completion at point to work need to add codeium-completion-at-point to completion-at-point-an

  ;; functions async company-backend is coming soon!

  ;; codeium-completion-at-point is autoloaded, but you can
  ;; optionally set a timer, which might speed up things as the
  ;; codeium local language server takes ~0.2s to start up
  ;; (add-hook 'emacs-startup-hook
  ;;  (lambda () (run-with-timer 0.1 nil #'codeium-init)))

  :config
  (setq use-dialog-box nil) ;; do not use popup boxes

  ;; if you don't want to use customize to save the api-key
  (setq codeium/metadata/api_key (password-store-get "API/Codeium"))

  ;; get codeium status in the modeline
  (setq codeium-mode-line-enable
        (lambda (api) (not (memq api '(CancelRequest Heartbeat AcceptCompletion)))))
  (add-to-list 'mode-line-format '(:eval (car-safe codeium-mode-line)) t)
  ;; alternatively for a more extensive mode-line
  ;; (add-to-list 'mode-line-format '(-50 "" codeium-mode-line) t)

  ;; use M-x codeium-diagnose to see apis/fields that would be sent to the local language server
  (setq codeium-api-enabled
        (lambda (api)
          (memq api '(GetCompletions Heartbeat CancelRequest GetAuthToken RegisterUser auth-redirect AcceptCompletion))))
  ;; you can also set a config for a single buffer like this:
  ;; (add-hook 'python-mode-hook
  ;;     (lambda ()
  ;;         (setq-local codeium/editor_options/tab_size 4)))

  ;; You can overwrite all the codeium configs!
  ;; for example, we recommend limiting the string sent to codeium for better performance
  (defun my-codeium/document/text ()
    (buffer-substring-no-properties (max (- (point) 3000) (point-min)) (min (+ (point) 1000) (point-max))))
  ;; if you change the text, you should also change the cursor_offset
  ;; warning: this is measured by UTF-8 encoded bytes
  (defun my-codeium/document/cursor_offset ()
    (codeium-utf8-byte-length
     (buffer-substring-no-properties (max (- (point) 3000) (point-min)) (point))))
  (setq codeium/document/text 'my-codeium/document/text)
  (setq codeium/document/cursor_offset 'my-codeium/document/cursor_offset)

  (let ((codeium-exe (executable-find "codeium_language_server")))
    (when codeium-exe
      (setq codeium-command-executable codeium-exe)))
  )

gptel

;; (package! gptel)

If get-password does not reply a proper key.. manually clear the auth-source cache with:

;; (auth-source-forget-all-cached)
(after! gptel
  (setq! gptel-default-mode 'org-mode
         ;; gptel-response-prefix-alist '((org-mode . "**** Answer"))
         gptel-api-key (password-store-get "API/OpenAI-emacs")
         ;; gptel-model 'gpt-4o
         gptel-model 'gemini-pro
         ;; 'gpt-4.5-preview
         gptel-log-level 'info
         ;; gptel-use-curl nil
         gptel-use-curl t
         gptel-stream t)

  (defun +gptel-font-lock-update (pos pos-end)
    ;; used with the gptel-post-response-functions hook but swollows the arguments
    (font-lock-update))

  ;; reload font-lock to fix syntax highlighting of org-babel src blocks
  (add-hook 'gptel-post-response-functions '+gptel-font-lock-update)

  (gptel-make-gemini "Gemini" :stream t
                     :key (password-store-get "API/Gemini-emacs"))

  (gptel-make-anthropic "Claude"          ;Any name you want
    :stream t                             ;Streaming responses
    :key (password-store-get "API/Claude-emacs"))

  (gptel-make-perplexity "Perplexity"          ;Any name you want
    :stream t                             ;Streaming responses
    :key (password-store-get "API/Perplexity-emacs-pro-ste.lendl"))

  (gptel-make-gh-copilot "Copilot")

  ;; OpenRouter offers an OpenAI compatible API
  (gptel-make-openai "OpenRouter"               ;Any name you want
    :host "openrouter.ai"
    :endpoint "/api/v1/chat/completions"
    :stream t
    :key (password-store-get "API/Openrouter-emacs")
    :models '(moonshotai/kimi-k2-thinking))

  (setf (alist-get 'cpp gptel-directives) "You are an expert C++ developer using C++20. ONLY use C++20 features availible in gcc12.
Do not use concepts. For functions, methods and variables use the style 'auto method() -> RetType'
Reply concisely. Wrap source code in a ```cpp block.")
  )

Claude Code IDE

(package! claude-code-ide
  :recipe (:host github
           :repo "manzaltu/claude-code-ide.el"))
(use-package! claude-code-ide
  :commands (claude-code-ide-menu)
  :config
  ;; (stfl/setup-api-keys)
  (setq! claude-code-ide-terminal-backend 'vterm
         claude-code-ide-switch-tab-on-ediff t
         claude-code-ide-focus-claude-after-ediff t)
  (claude-code-ide-emacs-tools-setup)) ; Optionally enable Emacs MCP tools

Agent Shell

(package! shell-maker)
(package! acp)
(package! agent-shell)
(require 'acp)
(require 'agent-shell)

(use-package agent-shell)