Files
emacs-doom/config.el

699 lines
26 KiB
EmacsLisp

;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
;;; ============================================================
;;; USER SETTINGS
;;; ============================================================
;; User identity
(setq user-full-name "Martin Sukany"
user-mail-address "martin@sukany.cz")
;; Theme and font
(setq doom-theme 'modus-vivendi-deuteranopia
doom-font (font-spec :family "JetBrains Mono" :size 14)
doom-variable-pitch-font nil)
;; Line numbers
(setq display-line-numbers-type t)
;;; ============================================================
;;; UI / DISPLAY
;;; ============================================================
;; Mouse and focus behavior
(setq mouse-autoselect-window t
focus-follows-mouse t
select-enable-clipboard t
select-enable-primary t
inhibit-splash-screen t)
;; Modeline refresh rate
(setq doom-modeline-refresh-rate 1.0)
;; Which-key delays
(setq which-key-idle-delay 0.8
which-key-idle-secondary-delay 0.05)
;; Auto-save: disable the #file# noise; save all buffers on idle instead
(setq auto-save-default nil)
(defun my/save-all-buffers ()
(save-some-buffers t))
(run-with-idle-timer 10 t #'my/save-all-buffers)
;; Centered cursor mode
;; Disabled — conflicts with macOS Zoom focus follower (causes screen jumping)
(use-package! centered-cursor-mode
:config
(setq ccm-vpos-init 0.5) ;; 0.5 = center of window
;; (global-centered-cursor-mode +1)
)
;; Performance: GC tuning
(setq gc-cons-threshold (* 100 1024 1024) ;; 100 MB
gc-cons-percentage 0.6)
;; GCMH — Doom's GC manager
;; Increased idle delay to reduce GC-triggered redraws (avoids Zoom focus jumping)
(after! gcmh
(setq gcmh-idle-delay 'auto
gcmh-auto-idle-delay-factor 20 ;; default 10, raised for less frequent GC
gcmh-high-cons-threshold (* 200 1024 1024))) ;; 200 MB threshold
(add-hook 'focus-out-hook #'garbage-collect)
;; macOS Zoom accessibility: cancel the persp-mode 2.5s cache timer to minimize redraws
(run-with-timer 3 nil
(lambda ()
(when (and (boundp 'persp-frame-buffer-predicate-buffer-list-cache--timer)
(timerp persp-frame-buffer-predicate-buffer-list-cache--timer))
(cancel-timer persp-frame-buffer-predicate-buffer-list-cache--timer)
(setq persp-frame-buffer-predicate-buffer-list-cache--timer nil)
(message "persp-mode 2.5s cache timer cancelled for Zoom accessibility"))))
;; macOS clipboard integration via pbcopy/pbpaste (works in terminal Emacs too)
(defun my/pbcopy (text &optional _push)
"Send TEXT to the macOS clipboard using pbcopy."
(let ((process-connection-type nil))
(let ((proc (start-process "pbcopy" "*pbcopy*" "pbcopy")))
(process-send-string proc text)
(process-send-eof proc))))
(defun my/pbpaste ()
"Return text from the macOS clipboard using pbpaste."
(when (executable-find "pbpaste")
(string-trim-right
(shell-command-to-string "pbpaste"))))
;; Emacs → system clipboard
(setq interprogram-cut-function #'my/pbcopy)
;; System clipboard → Emacs
(setq interprogram-paste-function #'my/pbpaste)
;; Let Evil use the clipboard (y/d/c operations go to system clipboard)
(after! evil
(setq evil-want-clipboard t))
;; PATH fix for MacTeX
(setenv "PATH" (concat "/Library/TeX/texbin:" (getenv "PATH")))
(add-to-list 'exec-path "/Library/TeX/texbin")
;; Disable TLS verification — WARNING: insecure, use only in trusted environments
(setq gnutls-verify-error nil)
(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")
;; Org clock idle time
(setq org-idle-time 1.0)
;;; ============================================================
;;; ORG MODE — CORE
;;; ============================================================
(after! org
;; Required packages
(require 'ox-hugo)
;; Org directory and default notes file
(setq org-directory "~/org/")
(setq org-default-notes-file (expand-file-name "inbox.org" org-directory))
;; TODO keyword states
(setq org-todo-keywords
'((sequence "TODO(t)" "NEXT(n)" "WAIT(w@/!)" "|" "DONE(d!)" "CANCELLED(c@)")))
;; Log completion time when a task is marked DONE
(setq org-log-done 'time)
;; Refile targets: all agenda files up to 5 levels deep
(setq org-refile-targets '((org-agenda-files :maxlevel . 5))
org-outline-path-complete-in-steps nil
org-refile-use-outline-path 'file)
;; Helper: return absolute path to a file inside org-directory
(defun ms/org-file (name)
"Return absolute path to NAME inside `org-directory`."
(expand-file-name name org-directory))
;; Helper: return path to project.org in current Projectile project (if it exists)
(defun my/project-org-file ()
"Return path to ./project.org in the current Projectile project, if it exists."
(when-let ((root (projectile-project-root)))
(let ((f (expand-file-name "project.org" root)))
(when (file-exists-p f) f))))
;; Restore window layout after closing a capture buffer
(setq org-capture-restore-window-after-quit t)
;; Update all dynamic blocks before export
(add-hook 'org-export-before-processing-hook
(lambda (_backend)
(org-update-all-dblocks))))
;;; ============================================================
;;; ORG MODE — CAPTURE
;;; ============================================================
(after! org
(setq org-capture-templates
`(
;; Inbox task
("i" "Inbox task" entry
(file ,(ms/org-file "inbox.org"))
"* TODO %?\n%U\n%a\n")
;; Note under inbox
("n" "Note" entry
(file+headline ,(ms/org-file "inbox.org") "Notes")
"* %?\n%U\n%a\n")
;; Project task
("p" "Project task" entry
(file ,(ms/org-file "inbox.org"))
"* TODO %? :project:\n%U\n%a\n")
;; Subtask under the currently clocked item
("s" "Clocked subtask" entry (clock)
"* TODO %?\n%U\n%a\n%i"
:empty-lines 1)
;; Journal entry in journal.org (datetree), with time tracking
("j" "Journal" entry
(file+olp+datetree ,(ms/org-file "journal.org"))
"\n* %<%I:%M %p> - Journal :journal:\n\n%?\n\n"
:clock-in :clock-resume
:empty-lines 1)
;; Meeting in journal.org (datetree), with time tracking
("m" "Meeting" entry
(file+olp+datetree ,(ms/org-file "journal.org"))
"* %<%I:%M %p> - %^{Meeting title} :meetings:\nContext: %a\n\n%?\n\n"
:clock-in :clock-resume
:empty-lines 1)
;; Email check entry in journal.org (datetree), with time tracking
("e" "Checking Email" entry
(file+olp+datetree ,(ms/org-file "journal.org"))
"* Checking Email :email:\n\n%?"
:clock-in :clock-resume
:empty-lines 1)
;; Weight metric logged as a table row in metrics.org
("w" "Weight" table-line
(file+headline ,(ms/org-file "metrics.org") "Weight")
"| %U | %^{Weight} | %^{Notes} |"
:kill-buffer t)
)))
;;; ============================================================
;;; ORG MODE — AGENDA
;;; ============================================================
(after! org
;; Files scanned by the agenda
(setq org-agenda-files (list org-directory
(expand-file-name "projects" org-directory)
(expand-file-name "roam" org-directory)
(expand-file-name "notes" org-directory)))
;; Elfeed feed list (kept here since it depends on org-directory)
(setq rmh-elfeed-org-files
(list (expand-file-name "elfeed.org" org-directory))))
;;; ============================================================
;;; ORG MODE — EXPORT / LATEX
;;; ============================================================
;; Count the number of data columns in an Org table line
(defun my/org-count-table-columns (line)
"Count the number of data columns in Org table LINE."
(length (cl-remove-if
(lambda (s) (string-match-p "^-*$" (string-trim s)))
(cdr (butlast (split-string line "|"))))))
;; Generate LaTeX column spec for tabularx: first column 'l', rest 'Y' (centered X)
(defun my/org-table-attr-latex-spec (ncols)
"Generate tabularx column spec for NCOLS columns: first l, rest Y."
(concat "l" (make-string (max 0 (1- ncols)) ?Y)))
;; Automatically insert #+ATTR_LATEX tabularx before each table on LaTeX export.
;; Usage: export with SPC m e l p — tables are wrapped automatically, no manual steps needed.
;; To customize the column spec, edit `my/org-table-attr-latex-spec'.
(defun my/org-auto-tabularx (backend)
"Automatically add #+ATTR_LATEX tabularx before every table during LaTeX export."
(when (org-export-derived-backend-p backend 'latex)
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(cond
;; Line starts with | — might be the beginning of a table
((looking-at "^|")
(let ((prev-line (save-excursion
(forward-line -1)
(buffer-substring-no-properties
(line-beginning-position) (line-end-position)))))
;; Only on the FIRST row of the table (previous line does NOT start with |)
(when (not (string-match-p "^|" prev-line))
;; Insert only if #+ATTR_LATEX is not already present
(when (not (string-match-p "^#\\+ATTR_LATEX" prev-line))
(let* ((table-line (buffer-substring-no-properties
(line-beginning-position) (line-end-position)))
(ncols (my/org-count-table-columns table-line))
(spec (my/org-table-attr-latex-spec ncols))
(attr (format "#+ATTR_LATEX: :environment tabularx :width \\textwidth :align %s\n"
spec)))
(when (> ncols 0)
(insert attr))))))
(forward-line))
(t
(forward-line)))))))
;; Register the hook — fires before each export
(add-hook 'org-export-before-processing-hook #'my/org-auto-tabularx)
;; Optional: enable booktabs style (horizontal rules in tables)
;; (setq org-latex-tables-booktabs t)
;; Python for Org Babel
(setq python-shell-interpreter "python3")
(after! org
(setq org-babel-python-command "python3")
(require 'ob-python))
;;; ============================================================
;;; ORG MODE — CUSTOM BEHAVIOR
;;; ============================================================
;; Org Agenda: move cursor to the task name after each navigation step.
;; After each n/p (or j/k in Evil mode), the cursor skips the TODO keyword
;; and optional priority marker [#A], landing directly on the task title.
(defun my/org-agenda-goto-task-name (&rest _)
"Move cursor to the task name — past the TODO keyword and priority [#A]."
(when (get-text-property (line-beginning-position) 'org-hd-marker)
(beginning-of-line)
(let* ((bol (point))
(eol (line-end-position))
(todo-end nil)
(pos bol))
;; Find the end of the TODO keyword by its face (org-todo or org-agenda-done)
(while (< pos eol)
(let* ((face (get-text-property pos 'face))
(next (or (next-single-property-change pos 'face nil eol) eol)))
(when (and face
(or (and (symbolp face)
(memq face '(org-todo org-agenda-done)))
(and (listp face)
(cl-intersection face '(org-todo org-agenda-done)))))
(setq todo-end next))
(setq pos next)))
;; Move past the TODO keyword and optional priority marker [#X]
(when todo-end
(goto-char todo-end)
(skip-chars-forward " \t")
(when (looking-at "\\[#.\\][ \t]+")
(goto-char (match-end 0)))))))
(advice-add 'org-agenda-next-line :after #'my/org-agenda-goto-task-name)
(advice-add 'org-agenda-previous-line :after #'my/org-agenda-goto-task-name)
;;; ============================================================
;;; OTHER PACKAGES
;;; ============================================================
;; --- Dired ---
(after! dired
(put 'dired-find-alternate-file 'disabled nil)
(map! :map dired-mode-map
"RET" #'dired-find-alternate-file
"^" #'dired-up-directory))
;; --- PlantUML ---
(add-to-list 'auto-mode-alist '("\\.puml\\'" . plantuml-mode))
(add-to-list 'auto-mode-alist '("\\.plantuml\\'" . plantuml-mode))
(after! plantuml-mode
(setq plantuml-default-exec-mode 'server
plantuml-server-url "https://www.plantuml.com/plantuml"
plantuml-output-type "svg"
plantuml-verbose t))
(defun my/plantuml-encode-hex (text)
"Encode TEXT using PlantUML HEX encoding: ~h + hex(UTF-8 bytes)."
(let* ((utf8 (encode-coding-string text 'utf-8 t)))
(concat "~h"
(apply #'concat
(mapcar (lambda (b) (format "%02x" b))
(append utf8 nil))))))
(defun my/plantuml-fix-png-header (file)
"Remove any bytes before the PNG signature in FILE."
(let ((sig (unibyte-string #x89 ?P ?N ?G ?\r ?\n #x1a ?\n)))
(with-temp-buffer
(set-buffer-multibyte nil)
(insert-file-contents-literally file)
(goto-char (point-min))
(unless (looking-at (regexp-quote sig))
(let ((pos (search-forward sig nil t)))
(unless pos (user-error "PNG signature not found"))
(delete-region (point-min) (- pos (length sig)))
(let ((coding-system-for-write 'binary))
(write-region (point-min) (point-max) file nil 'silent)))))))
(defun my/plantuml-render-server (type)
"Render current .puml buffer via PlantUML server to PNG or SVG."
(interactive (list (completing-read "Type: " '("png" "svg") nil t "png")))
(unless buffer-file-name (user-error "Open a .puml file first"))
(let* ((text (buffer-substring-no-properties (point-min) (point-max)))
(encoded (my/plantuml-encode-hex text))
(server (string-remove-suffix "/" plantuml-server-url))
(url (format "%s/%s/%s" server type encoded))
(out (concat (file-name-sans-extension buffer-file-name) "." type)))
(url-copy-file url out t)
(when (string-equal type "png")
(my/plantuml-fix-png-header out))
(message "PlantUML saved: %s" out)
out))
(after! plantuml-mode
(define-key plantuml-mode-map (kbd "C-c C-p")
(lambda () (interactive) (my/plantuml-render-server "png")))
(define-key plantuml-mode-map (kbd "C-c C-s")
(lambda () (interactive) (my/plantuml-render-server "svg"))))
;; --- GPTel + OpenWebUI (OpenRouter backend) ---
(use-package! gptel
:config
;; API key from environment variable — no keys in config
(defun my/openwebui-key ()
(or (getenv "OPENWEBUI_API_KEY")
(user-error "Missing OPENWEBUI_API_KEY env var")))
;; Fetch model list from OpenWebUI /api/models
(defun my/openwebui-fetch-model-ids ()
"Return list of model ids from OpenWebUI /api/models (field data[].id)."
(require 'url)
(require 'json)
(let* ((url-request-method "GET")
(url-request-extra-headers
`(("Authorization" . ,(concat "Bearer " (funcall #'my/openwebui-key))))))
(with-current-buffer (url-retrieve-synchronously
"https://ai.apps.sukany.cz/api/models" t t 15)
(goto-char (point-min))
(re-search-forward "\n\n" nil 'move) ;; skip HTTP headers
(let* ((json-object-type 'alist)
(json-array-type 'list)
(json-key-type 'symbol)
(obj (json-read))
(data (alist-get 'data obj))
(ids (delq nil (mapcar (lambda (it) (alist-get 'id it)) data))))
(kill-buffer (current-buffer))
ids))))
(defvar my/openwebui-models-cache nil)
(defun my/openwebui-models ()
"Cached list of model ids. Falls back to a minimal list if the API fails."
(or my/openwebui-models-cache
(setq my/openwebui-models-cache
(condition-case err
(my/openwebui-fetch-model-ids)
(error
(message "OpenWebUI models fetch failed: %s" err)
;; Fallback model list
'("openai/gpt-4o-mini" "openai/gpt-4.1-mini"))))))
(defun my/openwebui-refresh-models ()
"Clear cache and refetch the OpenWebUI model list."
(interactive)
(setq my/openwebui-models-cache nil)
(message "OpenWebUI models refreshed: %d" (length (my/openwebui-models))))
;; Backend: OpenWebUI (OpenAI-compatible chat/completions endpoint)
(setq gptel-backend
(gptel-make-openai "OpenWebUI"
:host "ai.apps.sukany.cz"
:protocol "https"
:key #'my/openwebui-key
:endpoint "/api/chat/completions"
:stream t
:curl-args '("--http1.1") ;; more stable via reverse proxy/ingress
:models (my/openwebui-models)))
;; Default model: lightweight and practical
(let* ((models (my/openwebui-models))
(preferred "openai/gpt-5-mini"))
(setq gptel-model (if (member preferred models)
preferred
(car models))))
;; Presets for quick task-based model switching
(gptel-make-preset 'fast
:description "Default (fast/cheap) — everyday use"
:backend "OpenWebUI"
:model "openai/gpt-4o-mini"
:system "Reply in Czech. Be specific and step-by-step. No padding."
:temperature 0.2)
(gptel-make-preset 'coding
:description "Code / refactor / review (stronger model when needed)"
:backend "OpenWebUI"
:model "openai/gpt-4.1-mini"
:system "You are a strict code reviewer. Suggest concrete changes and flag risks."
:temperature 0.1)
(gptel-make-preset 'deep
:description "Heavy analysis / architecture"
:backend "OpenWebUI"
:model "openai/gpt-4.1"
:system "Proceed systematically. Give variants, tradeoffs and recommendations."
:temperature 0.2)
;; Debug logging (uncomment if needed):
;; (setq gptel-log-level 'debug)
)
;; CLI helper: invoke gptel from the command line via `emacs --batch'
(defun my/gptel-cli (prompt &optional model system)
"Send PROMPT via gptel and print the response to stdout."
(require 'gptel)
(let* ((done nil)
(result nil)
(gptel-model (or model gptel-model))
(gptel--system-message (or system gptel--system-message)))
(gptel-request prompt
:callback (lambda (response _info)
(setq result response)
(setq done t)))
(while (not done)
(accept-process-output nil 0.05))
(princ result)))
;; GPTel keybindings: SPC o g ...
(after! gptel
(map! :leader
(:prefix ("o g" . "GPTel")
:desc "GPTel send (region or buffer)" "s" #'gptel-send
:desc "GPTel menu (model/scope/preset)" "m" #'gptel-menu
:desc "GPTel chat buffer" "c" #'gptel
:desc "GPTel abort request" "x" #'gptel-abort
:desc "Refresh OpenWebUI models" "R" #'my/openwebui-refresh-models)))
;; --- Emacspeak: robust ON/OFF for Doom ---
;; Default: OFF (will not auto-restart on its own)
;; SPC t s → Speech ON
;; SPC t S → Speech OFF
(defconst my/emacspeak-dir (expand-file-name "~/.emacspeak"))
(defconst my/emacspeak-wrapper (expand-file-name "~/.local/bin/emacspeak-mac"))
;; Path to the speech server binary
(setq dtk-program my/emacspeak-wrapper)
;; State flags
(defvar my/emacspeak-loaded nil)
(defvar my/emacspeak-enabled nil)
;; Hard inhibit: when non-nil, Emacspeak is NOT allowed to start/restart the server
(defvar my/emacspeak-inhibit-server t)
(defun my/emacspeak--ensure-loaded ()
"Load Emacspeak once, safely, without breaking Doom startup."
(unless my/emacspeak-loaded
(setq my/emacspeak-loaded t)
(setq emacspeak-directory my/emacspeak-dir)
(load-file (expand-file-name "lisp/emacspeak-setup.el" emacspeak-directory))
;; Install inhibition advices to prevent auto-restart when speech is OFF
(with-eval-after-load 'dtk-speak
(dolist (fn '(dtk-initialize dtk-start-process dtk-speak))
(when (fboundp fn)
(advice-add
fn :around
(lambda (orig &rest args)
(if my/emacspeak-inhibit-server
nil ;; OFF mode: do nothing, do not restart the speaker
(apply orig args)))))))))
(defun my/emacspeak-on ()
"Enable speech and allow the server to start."
(interactive)
(setq my/emacspeak-inhibit-server nil)
(my/emacspeak--ensure-loaded)
(setq my/emacspeak-enabled t)
(when (fboundp 'dtk-restart)
(ignore-errors (dtk-restart)))
(when (fboundp 'dtk-speak)
(ignore-errors (dtk-speak "Emacspeak on.")))
(message "Emacspeak ON"))
(defun my/emacspeak-off ()
"Disable speech robustly: stop output and prevent auto-restart."
(interactive)
(setq my/emacspeak-enabled nil)
(setq my/emacspeak-inhibit-server t)
(when (fboundp 'dtk-stop)
(ignore-errors (dtk-stop)))
(when (boundp 'dtk-speaker-process)
(let ((p dtk-speaker-process))
(when (processp p)
(ignore-errors (set-process-sentinel p nil))
(ignore-errors (delete-process p))))
(setq dtk-speaker-process nil))
(message "Emacspeak OFF (server restart inhibited)"))
;; Doom leader keys for Emacspeak toggle
(map! :leader
(:prefix ("t" . "toggle")
:desc "Speech ON" "s" #'my/emacspeak-on
:desc "Speech OFF" "S" #'my/emacspeak-off))
;; Emacspeak defaults: punctuation mode and typing feedback
(with-eval-after-load 'dtk-speak
(setq dtk-speech-rate-base 300)
(setq-default dtk-punctuation-mode 'none))
(with-eval-after-load 'emacspeak
;; Echo words and lines while typing, not individual characters
(setq-default emacspeak-character-echo nil)
(setq-default emacspeak-word-echo t)
(setq-default emacspeak-line-echo t))
;; Describe buffer-local bindings shortcut
(map! :leader
(:prefix ("h" . "help")
:desc "Describe bindings (buffer-local)" "B" #'describe-bindings))
;; Global default speech rate; applied after TTS is initialized/restarted
(setq dtk-default-speech-rate 400)
(with-eval-after-load 'dtk-speak
(defun my/dtk-apply-global-default-rate (&rest _)
"Apply the global default speech rate after TTS init or restart."
(when (fboundp 'dtk-set-rate)
;; Non-nil prefix arg sets the GLOBAL default (per Emacspeak manual)
(ignore-errors (dtk-set-rate dtk-default-speech-rate t))))
(advice-add 'dtk-initialize :after #'my/dtk-apply-global-default-rate))
;; --- Elfeed (RSS reader) ---
(map! :leader
:desc "Elfeed" "o r" #'elfeed)
(after! elfeed
(require 'elfeed-org)
(elfeed-org))
;; --- Corfu (modern completion UI) ---
(after! corfu
(setq corfu-auto t
corfu-auto-delay 0.15
corfu-auto-prefix 2
corfu-cycle t
corfu-preselect 'prompt
corfu-quit-no-match 'separator
corfu-preview-current nil)
(global-corfu-mode))
;; --- Cape (completion-at-point sources) ---
(use-package! cape
:after corfu
:config
(defun martin/cape-capf-setup ()
"Register completion sources usable almost everywhere (without LSP)."
(add-to-list 'completion-at-point-functions #'cape-dabbrev 0) ;; words from open buffers
(add-to-list 'completion-at-point-functions #'cape-file 0) ;; file paths
(add-to-list 'completion-at-point-functions #'cape-keyword 0) ;; keywords (mainly prog-mode)
(add-to-list 'completion-at-point-functions #'cape-elisp-symbol 0)) ;; Elisp symbols
(add-hook 'prog-mode-hook #'martin/cape-capf-setup)
(add-hook 'text-mode-hook #'martin/cape-capf-setup))
;; Corfu popup in terminal (iTerm2 / ssh / tmux)
(use-package! corfu-terminal
:when (not (display-graphic-p))
:after corfu
:config
(corfu-terminal-mode +1))
;; --- TRAMP ---
(after! tramp
(setq projectile-git-command "git ls-files -zco --exclude-standard"
projectile-indexing-method 'alien))
;; Disable VC and Projectile over TRAMP — primary cause of hangs
(setq vc-ignore-dir-regexp
(format "%s\\|%s" vc-ignore-dir-regexp tramp-file-name-regexp))
;; Disable Projectile entirely for remote paths
(defadvice projectile-project-root (around ignore-remote first activate)
(unless (file-remote-p default-directory) ad-do-it))
;; TRAMP cache: avoid repeated expensive remote queries
(setq remote-file-name-inhibit-cache nil
tramp-verbose 1)
;; --- Projectile ---
(after! projectile
(setq projectile-enable-caching nil
projectile-indexing-method 'alien)
(when (executable-find "fd")
(setq projectile-generic-command
"fd . -0 --type f --hidden --follow --exclude .git --color=never")))
;; --- mu4e ---
(add-to-list 'load-path (expand-file-name "/opt/homebrew/opt/mu/share/emacs/site-lisp/mu/mu4e"))
(after! mu4e
(setq mu4e-maildir (expand-file-name "~/.mail")
mu4e-get-mail-command "mbsync personal"
mu4e-update-interval 300
mu4e-change-filenames-when-moving t
mu4e-view-show-images t
mu4e-sent-folder "/personal/Sent"
mu4e-drafts-folder "/personal/Drafts"
mu4e-trash-folder "/personal/Trash"
mu4e-refile-folder "/personal/Archive"))
(after! mu4e
(setq sendmail-program "msmtp"
message-send-mail-function #'message-send-mail-with-sendmail
mail-specify-envelope-from t
message-sendmail-envelope-from 'header))
(after! mu4e
;; Thread view
(setq mu4e-headers-show-threads t
mu4e-headers-include-related t)
;; Header column layout
(setq mu4e-headers-fields
'((:human-date . 12)
(:flags . 6)
(:from . 22)
(:subject)))
;; Fancy thread tree characters and mark-for-thread support
(setq mu4e-use-fancy-chars t)
(setq mu4e-headers-mark-for-thread t))