;;; $DOOMDIR/config.el -*- lexical-binding: t; -*- ;;; ============================================================ ;;; USER IDENTITY ;;; ============================================================ (setq user-full-name "Martin Sukany" user-mail-address "martin@sukany.cz") ;; CalDAV calendar IDs (edit here to update sync targets) (defconst my/caldav-url "https://cal.apps.sukany.cz/dav.php/calendars/martin") (defconst my/caldav-id-suky "default") (defconst my/caldav-id-placeholders "4C748EE5-ECFF-4D4A-A72E-6DE37BAADEB3") (defconst my/caldav-id-family "family") (defconst my/caldav-id-klara "klara") ;; Trust all TLS certificates (corporate MITM proxy with intermediate CA) (setq gnutls-verify-error nil) (setq tls-checktrust nil) (setq network-security-level 'low) ;;; ============================================================ ;;; THEME & FONT ;;; ============================================================ (setq doom-theme 'modus-vivendi-deuteranopia doom-variable-pitch-font nil) ;; Font: JetBrains Mono preferred; fallback to Menlo (always on macOS) ;; Install: brew install --cask font-jetbrains-mono (setq doom-font (if (find-font (font-spec :name "JetBrains Mono")) (font-spec :family "JetBrains Mono" :size 14) (font-spec :family "Menlo" :size 14))) (setq display-line-numbers-type t) ;; Start Emacs maximized on every launch (macOS: fills screen, keeps menu bar) (add-to-list 'initial-frame-alist '(fullscreen . maximized)) ;;; ============================================================ ;;; UI & DISPLAY ;;; ============================================================ (setq doom-modeline-refresh-rate 1.0) (setq which-key-idle-delay 0.8 which-key-idle-secondary-delay 0.05) ;; Centered cursor mode — safe with macOS Zoom set to "Follow mouse cursor" ;; (NOT "Follow keyboard focus" — that causes viewport jumping) (use-package! centered-cursor-mode :config (setq ccm-vpos-init 0.5 ccm-step-size 2 ccm-recenter-at-end-of-file t) (define-globalized-minor-mode my/global-ccm centered-cursor-mode (lambda () (unless (memq major-mode '(vterm-mode eshell-mode term-mode treemacs-mode pdf-view-mode)) (centered-cursor-mode 1)))) (my/global-ccm +1)) ;;; ============================================================ ;;; MACOS / PLATFORM ;;; ============================================================ (setq mouse-autoselect-window nil focus-follows-mouse nil select-enable-clipboard t select-enable-primary t inhibit-splash-screen t) ;; PATH: ensure GUI Emacs sees the same paths as the terminal. ;; macOS GUI apps do not inherit the shell PATH. (let ((extra-paths '("/opt/local/bin" "/opt/local/sbin" "/opt/homebrew/bin" "/opt/homebrew/sbin" "/usr/local/bin" "/Library/TeX/texbin"))) (dolist (p (reverse extra-paths)) (setenv "PATH" (concat p ":" (getenv "PATH"))) (add-to-list 'exec-path p))) ;; macOS clipboard integration via pbcopy/pbpaste (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")))) (setq interprogram-cut-function #'my/pbcopy interprogram-paste-function #'my/pbpaste) ;; Let Evil use the system clipboard (after! evil (setq evil-want-clipboard t) ;; Ensure dashboard buffer starts in normal state (required for SPC leader) (evil-set-initial-state '+doom-dashboard-mode 'normal)) ;; Standard macOS modifier keys for GUI Emacs (when (display-graphic-p) (setq mac-command-modifier 'super mac-option-modifier 'meta mac-right-option-modifier 'none)) ;; Disable mouse clicks in GUI (prevent accidental cursor movement) ;; Scroll wheel events are NOT disabled — scrolling works normally. (when (display-graphic-p) (dolist (key '([mouse-1] [mouse-2] [mouse-3] [double-mouse-1] [double-mouse-2] [double-mouse-3] [triple-mouse-1] [triple-mouse-2] [triple-mouse-3] [drag-mouse-1] [drag-mouse-2] [drag-mouse-3] [down-mouse-1] [down-mouse-2] [down-mouse-3])) (global-set-key key #'ignore)) (setq mouse-highlight nil)) ;; macOS scroll wheel: standard mwheel.el with conservative settings. ;; pixel-scroll-precision-mode is NOT used (targets X11, breaks NS/Cocoa). (when (display-graphic-p) (require 'mwheel) (setq mouse-wheel-scroll-amount '(3 ((shift) . 1) ((meta) . 0) ((control) . text-scale)) mouse-wheel-progressive-speed nil mouse-wheel-follow-mouse t mouse-wheel-tilt-scroll t mouse-wheel-flip-direction nil) (global-set-key [wheel-up] #'mwheel-scroll) (global-set-key [wheel-down] #'mwheel-scroll)) ;; Cancel persp-mode's 2.5s cache timer after startup ;; (reduces unnecessary redraws that cause macOS Zoom to jump) (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)))) ;;; ============================================================ ;;; CLIPBOARD IMAGE PASTE (macOS — cmd+v) ;;; ============================================================ ;; Requires: brew install pngpaste ;; cmd+v: clipboard has image → saves to attachments/ → inserts link ;; clipboard has text → normal paste (yank) (defun my/clipboard-has-image-p () "Return non-nil if macOS clipboard contains an image (requires pngpaste)." (and (executable-find "pngpaste") (let ((tmp (make-temp-file "emacs-imgcheck" nil ".png"))) (prog1 (= 0 (call-process "pngpaste" nil nil nil tmp)) (ignore-errors (delete-file tmp)))))) (defun my/paste-image-from-clipboard () "Save clipboard image to attachments/ and insert link at point." (interactive) (let* ((base-dir (if buffer-file-name (file-name-directory (buffer-file-name)) default-directory)) (attach-dir (expand-file-name "attachments" base-dir)) (filename (format-time-string "image-%Y%m%d-%H%M%S.png")) (filepath (expand-file-name filename attach-dir)) (relpath (concat "attachments/" filename))) (make-directory attach-dir t) (if (= 0 (call-process "pngpaste" nil nil nil filepath)) (progn (insert (pcase major-mode ('org-mode (format "[[./%s]]" relpath)) ('markdown-mode (format "![%s](./%s)" filename relpath)) (_ relpath))) (message "Image saved: %s" relpath)) (error "pngpaste failed — no image in clipboard?")))) (defun my/smart-paste () "Paste image if clipboard has one, else normal yank." (interactive) (if (my/clipboard-has-image-p) (my/paste-image-from-clipboard) (yank))) (map! :after org :map org-mode-map "s-v" #'my/smart-paste) (map! :after markdown-mode :map markdown-mode-map "s-v" #'my/smart-paste) ;;; ============================================================ ;;; PERFORMANCE & GC ;;; ============================================================ (setq gc-cons-threshold (* 100 1024 1024) gc-cons-percentage 0.6) (after! gcmh (setq gcmh-idle-delay 'auto gcmh-auto-idle-delay-factor 20 gcmh-high-cons-threshold (* 200 1024 1024))) (add-hook 'focus-out-hook #'garbage-collect) ;; Auto-save all buffers on idle (replaces noisy #file# autosave) (setq auto-save-default nil) (defun my/save-all-buffers () (save-some-buffers t)) (run-with-idle-timer 10 t #'my/save-all-buffers) ;; TLS: also allow TLS 1.2 fallback for self-signed local services (setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3") ;;; ============================================================ ;;; ORG MODE — CORE ;;; ============================================================ ;; Czech holidays only (no US/Hebrew/Christian defaults) (setq calendar-holidays '((holiday-fixed 1 1 "New Year / Czech Independence Restoration Day") (holiday-easter-etc -2 "Good Friday") (holiday-easter-etc 1 "Easter Monday") (holiday-fixed 5 1 "Labour Day") (holiday-fixed 5 8 "Victory Day") (holiday-fixed 7 5 "Saints Cyril and Methodius Day") (holiday-fixed 7 6 "Jan Hus Day") (holiday-fixed 9 28 "Czech Statehood Day") (holiday-fixed 10 28 "Czechoslovak Independence Day") (holiday-fixed 11 17 "Freedom and Democracy Day") (holiday-fixed 12 24 "Christmas Eve") (holiday-fixed 12 25 "Christmas Day") (holiday-fixed 12 26 "St. Stephen's Day"))) (after! org (require 'ox-hugo) (setq org-directory "~/org/") (setq org-default-notes-file (expand-file-name "inbox.org" org-directory)) (defun ms/org-file (name) "Return absolute path to NAME inside `org-directory`." (expand-file-name name org-directory)) (setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "WAIT(w@/!)" "|" "DONE(d!)" "CANCELLED(c@)"))) (setq org-log-done 'time) (setq org-refile-targets '((org-agenda-files :maxlevel . 5)) org-outline-path-complete-in-steps nil org-refile-use-outline-path 'file) (defun my/project-org-file () "Return path to ./project.org in 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)))) ;; Update all dynamic blocks before export (add-hook 'org-export-before-processing-hook (lambda (_backend) (org-update-all-dblocks))) ;; Visual: hide markup, pretty entities, compact tags (setq org-startup-indented nil ; conflicts with org-modern star display org-startup-folded 'content ; show all headings, hide body text org-hide-emphasis-markers t org-pretty-entities t org-ellipsis " ▾" org-auto-align-tags nil org-tags-column 0) (setq org-capture-restore-window-after-quit t)) ;;; ============================================================ ;;; ORG MODE — CAPTURE ;;; ============================================================ (after! org (setq org-capture-templates `(("i" "Inbox task" entry (file ,(ms/org-file "inbox.org")) "* TODO %?\n%U\n%a\n") ("n" "Note" entry (file+headline ,(ms/org-file "inbox.org") "Notes") "* %?\n%U\n%a\n") ("p" "Project task" entry (file ,(ms/org-file "inbox.org")) "* TODO %? :project:\n%U\n%a\n") ("s" "Clocked subtask" entry (clock) "* TODO %?\n%U\n%a\n%i" :empty-lines 1) ("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) ("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) ("e" "Checking Email" entry (file+olp+datetree ,(ms/org-file "journal.org")) "* Checking Email :email:\n\n%?" :clock-in :clock-resume :empty-lines 1) ("w" "Weight" table-line (file+headline ,(ms/org-file "metrics.org") "Weight") "| %U | %^{Weight} | %^{Notes} |" :kill-buffer t) ("c" "Calendar event" entry (file ,(ms/org-file "calendar_outbox.org")) "* %?\n%(my/calfw-capture-timestamp)\n" :empty-lines 1)))) ;; Ensure ox-icalendar exports active timestamps with times correctly (after! ox-icalendar (setq org-icalendar-with-timestamps 'active org-icalendar-timezone "Europe/Prague")) ;;; ============================================================ ;;; ORG MODE — AGENDA ;;; ============================================================ (after! org (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)))) ;;; ============================================================ ;;; ORG MODE — LATEX EXPORT ;;; ============================================================ ;; Table export filter: tabular -> tabularx with auto-width Y columns. ;; Skipped for beamer exports (beamer uses adjustbox on plain tabular). ;; Y column type defined in document.org template (RaggedRight + auto-width X). (defun my/org-latex-col-to-lyyy (spec) "Convert tabular column SPEC to lYYY: first col l, rest Y (auto-width)." (let ((ncols (max 1 (length (replace-regexp-in-string "[^lrcLRCpP]" "" spec))))) (if (= ncols 1) "Y" (concat "l" (make-string (1- ncols) ?Y))))) (defun my/org-latex-fix-tabularx (table backend _info) "Convert tabular/tabularx -> tabularx{linewidth}{lYYY} in LaTeX output. Skip for beamer exports — beamer uses adjustbox on plain tabular." (when (and (stringp table) (not (org-export-derived-backend-p backend 'beamer))) (with-temp-buffer (insert table) (goto-char (point-min)) (while (re-search-forward "\\\\begin{tabular[x]?}{\\([^}]*\\)}" nil t) (let* ((spec (match-string 1)) (new-spec (my/org-latex-col-to-lyyy spec)) (repl (concat "\\begin{tabularx}{\\linewidth}{" new-spec "}"))) (replace-match repl t t))) (goto-char (point-min)) (while (re-search-forward "\\\\end{tabular}" nil t) (replace-match "\\end{tabularx}" t t)) (buffer-string)))) (with-eval-after-load 'ox-latex (add-to-list 'org-export-filter-table-functions #'my/org-latex-fix-tabularx) (add-to-list 'org-latex-packages-alist '("" "tabularx"))) (defun my/org-ensure-tabularx-filter (backend) "Force ox-latex load and register tabularx filter before LaTeX export." (when (org-export-derived-backend-p backend 'latex) (require 'ox-latex) (add-to-list 'org-export-filter-table-functions #'my/org-latex-fix-tabularx) (add-to-list 'org-latex-packages-alist '("" "tabularx")))) (add-hook 'org-export-before-processing-hook #'my/org-ensure-tabularx-filter) ;; Ensure latexmk doesn't hang on errors (after! org (setq org-latex-pdf-process '("latexmk -f -pdf -%latex -interaction=nonstopmode -output-directory=%o %f"))) ;; LaTeX aux file cleanup after export (with-eval-after-load 'ox-latex (setq org-latex-remove-logfiles t org-latex-logfiles-extensions '("aux" "bbl" "bcf" "blg" "fdb_latexmk" "fls" "idx" "log" "nav" "out" "ptc" "run.xml" "snm" "synctex.gz" "toc" "vrb" "xdv"))) ;; Export directory routing: all exports go to ~/exports// (defun my/org-export-directory (extension) "Return ~/exports// directory for file EXTENSION, creating it if needed." (let* ((ext (downcase (string-trim-left extension "\\."))) (subdir (if (string= ext "tex") "pdf" ext)) (dir (expand-file-name (concat "exports/" subdir "/") "~"))) (make-directory dir t) dir)) (defun my/org-export-output-file-name (orig extension &optional subtreep pub-dir) "Route org exports to ~/exports// unless pub-dir is already set." (funcall orig extension subtreep (or pub-dir (my/org-export-directory extension)))) (advice-add 'org-export-output-file-name :around #'my/org-export-output-file-name) ;;; ============================================================ ;;; ORG MODE — CUSTOM BEHAVIOR ;;; ============================================================ ;; Agenda: position cursor at task name (after TODO keyword and priority) (defun my/org-agenda-all-keywords () "Return list of all org todo keyword strings (without shortcut suffixes)." (let (result) (dolist (seq org-todo-keywords result) (dolist (kw (cdr seq)) (unless (equal kw "|") (push (replace-regexp-in-string "(.*" "" kw) result)))))) (defun my/org-agenda-goto-task-name (&rest _) "Move cursor to the task name on the current org-agenda line." (when (get-text-property (line-beginning-position) 'org-hd-marker) (beginning-of-line) (let* ((eol (line-end-position)) (kw-re (regexp-opt (my/org-agenda-all-keywords) 'words))) (when (re-search-forward kw-re eol t) (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) ;; Also trigger on post-command-hook in agenda buffers (catches Evil j/k, ;; super-agenda navigation, and any other motion commands) (add-hook 'org-agenda-mode-hook (lambda () (add-hook 'post-command-hook #'my/org-agenda-goto-task-name nil t))) ;; Org buffer: snap cursor past TODO keyword/priority on headings in normal state (defun my/org-heading-snap-past-keyword () "In Evil normal state, snap cursor past TODO keyword and priority on org headings." (when (and (derived-mode-p 'org-mode) (evil-normal-state-p) (org-at-heading-p)) (let* ((kw-re (regexp-opt (my/org-agenda-all-keywords) 'words)) (task-start (save-excursion (beginning-of-line) (skip-chars-forward "* ") (when (looking-at kw-re) (goto-char (match-end 0)) (skip-chars-forward " \t") (when (looking-at "\\[#.\\][ \t]+") (goto-char (match-end 0))) (point))))) (when (and task-start (< (point) task-start)) (goto-char task-start))))) (add-hook 'org-mode-hook (lambda () (add-hook 'post-command-hook #'my/org-heading-snap-past-keyword nil t))) ;;; ============================================================ ;;; GPTEL — AI INTEGRATION (OpenWebUI / OpenRouter) ;;; ============================================================ (use-package! gptel :config ;; API key from environment variable (no secrets in config) (defun my/openwebui-key () (or (getenv "OPENWEBUI_API_KEY") (user-error "Missing OPENWEBUI_API_KEY env var"))) ;; Fetch available models from OpenWebUI /api/models (defun my/openwebui-fetch-model-ids () "Return list of model ids from OpenWebUI /api/models." (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) (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 () "Return cached list of model ids; falls back to a minimal list on failure." (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) '("openai/gpt-4o-mini" "openai/gpt-4.1-mini")))))) (defun my/openwebui-refresh-models () "Clear model cache and refetch from OpenWebUI." (interactive) (setq my/openwebui-models-cache nil) (message "OpenWebUI models refreshed: %d" (length (my/openwebui-models)))) ;; Register OpenWebUI as an OpenAI-compatible backend (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") :models (my/openwebui-models))) ;; Default model (let* ((models (my/openwebui-models)) (preferred "openai/gpt-5-mini")) (setq gptel-model (if (member preferred models) preferred (car models)))) ;; Presets for quick task-specific model switching (gptel-make-preset 'fast :description "Default (fast/cheap) — everyday work" :backend "OpenWebUI" :model "openai/gpt-4o-mini" :system "Reply in Czech. Be specific and step-by-step. No fluff." :temperature 0.2) (gptel-make-preset 'coding :description "Code / refactor / review" :backend "OpenWebUI" :model "openai/gpt-4.1-mini" :system "You are a strict code reviewer. Propose concrete changes and flag risks." :temperature 0.1) (gptel-make-preset 'deep :description "Complex analysis / architecture" :backend "OpenWebUI" :model "openai/gpt-4.1" :system "Work systematically. Provide alternatives, tradeoffs, and a recommendation." :temperature 0.2)) ;; CLI helper: call gptel from emacs --batch (defun my/gptel-cli (prompt &optional model system) "Send PROMPT via gptel and print 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 under SPC o g (after! gptel (map! :leader (:prefix ("o g" . "GPTel") :desc "Send (region or buffer)" "s" #'gptel-send :desc "Menu (model/scope/preset)" "m" #'gptel-menu :desc "Chat buffer" "c" #'gptel :desc "Abort request" "x" #'gptel-abort :desc "Refresh OpenWebUI models" "R" #'my/openwebui-refresh-models))) ;; GPTel region rewrite & org heading prompt (after! gptel (defun my/gptel-rewrite-region (beg end) "Send selected region to GPTel with 'improve text' instruction and replace with response." (interactive "r") (let ((text (buffer-substring-no-properties beg end))) (gptel-request (concat "Improve the following text. Return ONLY the improved text, nothing else:\n\n" text) :callback (lambda (response info) (if response (save-excursion (delete-region beg end) (goto-char beg) (insert response) (message "GPTel: text improved")) (message "GPTel rewrite failed: %s" (plist-get info :status))))))) (defun my/gptel-org-heading-prompt () "Send current org heading + content as context to GPTel chat." (interactive) (unless (derived-mode-p 'org-mode) (user-error "Only available in org-mode")) (let* ((heading (org-get-heading t t t t)) (content (save-excursion (org-back-to-heading t) (let ((beg (point))) (org-end-of-subtree t t) (buffer-substring-no-properties beg (point)))))) (gptel content) (message "GPTel: heading '%s' sent as context" heading))) (map! :leader (:prefix ("o g" . "GPTel") :desc "Rewrite region" "r" #'my/gptel-rewrite-region :desc "Org heading -> GPTel" "p" #'my/gptel-org-heading-prompt))) ;;; ============================================================ ;;; COMPLETION — CORFU + CAPE ;;; ============================================================ (after! corfu (setq corfu-auto t corfu-auto-delay 2.0 corfu-auto-prefix 3 ; need 3+ chars before popup corfu-cycle t corfu-preselect 'prompt corfu-quit-no-match 'separator corfu-preview-current nil) ;; Re-set delay after global-corfu-mode to override Doom defaults (add-hook 'global-corfu-mode-hook (lambda () (setq corfu-auto-delay 2.0))) (global-corfu-mode)) (use-package! cape :after corfu :config (defun martin/cape-capf-setup-text () "Cape sources for text modes (org, markdown) — no Elisp symbols." (add-to-list 'completion-at-point-functions #'cape-dabbrev 0) (add-to-list 'completion-at-point-functions #'cape-file 0)) (defun martin/cape-capf-setup-prog () "Cape sources for prog modes — includes Elisp symbols only in Elisp." (add-to-list 'completion-at-point-functions #'cape-dabbrev 0) (add-to-list 'completion-at-point-functions #'cape-file 0) (add-to-list 'completion-at-point-functions #'cape-keyword 0) (when (derived-mode-p 'emacs-lisp-mode) (add-to-list 'completion-at-point-functions #'cape-elisp-symbol 0))) (add-hook 'text-mode-hook #'martin/cape-capf-setup-text) (add-hook 'prog-mode-hook #'martin/cape-capf-setup-prog)) ;; Yasnippet-capf first in CAPF list so corfu shows/expands snippets (after! (yasnippet corfu) (dolist (hook '(org-mode-hook markdown-mode-hook text-mode-hook prog-mode-hook)) (add-hook hook (lambda () (add-hook 'completion-at-point-functions #'yasnippet-capf 0 t))))) ;; Corfu popup in terminal — only for Emacs < 31 (31+ handles it natively) (use-package! corfu-terminal :when (and (not (display-graphic-p)) (< emacs-major-version 31)) :after corfu :config (corfu-terminal-mode +1)) ;;; ============================================================ ;;; EMAIL — MU4E ;;; ============================================================ (add-to-list 'load-path (expand-file-name "/opt/homebrew/opt/mu/share/emacs/site-lisp/mu/mu4e")) (after! mu4e ;; --- Mailbox layout --- (setq mu4e-maildir "~/.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") ;; --- Headers view --- (setq mu4e-headers-show-threads t mu4e-headers-include-related t mu4e-use-fancy-chars t mu4e-headers-mark-for-thread t mu4e-headers-fields '((:human-date . 12) (:flags . 6) (:from-or-to . 25) (:subject)) mu4e-headers-sort-field :date mu4e-headers-sort-direction 'descending ;; Thread prefixes — fancy Unicode mu4e-headers-thread-single-orphan-prefix '("─>" . "─▶ ") mu4e-headers-thread-orphan-prefix '("┬>" . "┬▶ ") mu4e-headers-thread-connection-prefix '("│" . "│ ") mu4e-headers-thread-first-child-prefix '("├>" . "├▶ ") mu4e-headers-thread-last-child-prefix '("└>" . "└▶ ") mu4e-headers-thread-duplicate-prefix '("=" . "≡ ")) ;; --- Bookmarks --- ;; Keys: u=Unread i=Inbox d=Today w=Week ;; Note: ?d for Today avoids conflict with maildir shortcut ?t=Trash (setq mu4e-bookmarks '((:name "Unread" :query "flag:unread AND NOT maildir:/personal/Trash AND NOT maildir:/personal/Archive AND NOT maildir:/personal/Sent AND NOT maildir:/personal/Drafts AND NOT maildir:/personal/Spam" :key ?u) (:name "Inbox" :query "maildir:/personal/INBOX" :key ?i) (:name "Today" :query "date:today AND NOT maildir:/personal/Trash AND NOT maildir:/personal/Archive AND NOT maildir:/personal/Sent" :key ?d) (:name "Week" :query "date:7d..now AND NOT maildir:/personal/Trash AND NOT maildir:/personal/Archive AND NOT maildir:/personal/Sent" :key ?w))) ;; --- Maildir shortcuts (jump with 'j') --- ;; Keys: i=INBOX s=Sent T=Trash a=Archive ;; ?T (uppercase) for Trash avoids conflict with bookmark ?t (was Today) (setq mu4e-maildir-shortcuts '(("/personal/INBOX" . ?i) ("/personal/Sent" . ?s) ("/personal/Trash" . ?T) ("/personal/Archive" . ?a))) ;; --- Sending --- (setq sendmail-program "msmtp" message-send-mail-function #'message-send-mail-with-sendmail mail-specify-envelope-from t message-sendmail-envelope-from 'header) ;; --- Compose / reply --- ;; message-cite-function is a message-mode setting but configured here ;; because it only matters in the context of mu4e replies. (setq message-cite-function #'message-cite-original-without-signature message-signature-file (expand-file-name "~/.mail/signature") message-signature t) ;; --- Citation colors --- ;; gnus-cite-* : colors in the view (read) buffer ;; message-cited-text-*: colors in the compose (reply) buffer ;; Both use the same Dracula palette for visual consistency. ;; Duplicate face names are intentional — gnus and message-mode ;; use separate face systems even though they render the same content. (setq gnus-cite-face-list '(gnus-cite-1 gnus-cite-2 gnus-cite-3 gnus-cite-4)) (custom-set-faces! '(gnus-cite-1 :foreground "#8be9fd" :italic t) ; cyan — level 1 '(gnus-cite-2 :foreground "#bd93f9" :italic t) ; purple — level 2 '(gnus-cite-3 :foreground "#6272a4" :italic t) ; blue — level 3 '(gnus-cite-4 :foreground "#44475a" :italic t) ; grey — level 4+ '(message-cited-text-1 :foreground "#8be9fd" :italic t) '(message-cited-text-2 :foreground "#bd93f9" :italic t) '(message-cited-text-3 :foreground "#6272a4" :italic t) '(message-cited-text-4 :foreground "#44475a" :italic t)) ;; --- View: skip to message body --- ;; gnus-article-prepare-hook fires when the article buffer is ready, ;; covering both the initial render and navigation between messages. ;; No need for mu4e-view-mode-hook (that fires earlier, before content). (defun my/mu4e-view-goto-body () "Position cursor at message body, skipping RFC 2822 headers." (run-with-idle-timer 0.05 nil (lambda () (when-let ((buf (get-buffer "*mu4e-article*"))) (with-current-buffer buf (goto-char (point-min)) (while (and (not (eobp)) (looking-at "^\([A-Za-z-]+:\|[ \t]\)")) (forward-line 1)) (while (and (not (eobp)) (looking-at "^\s-*$")) (forward-line 1))))))) (add-hook 'gnus-article-prepare-hook #'my/mu4e-view-goto-body) ;; --- Headers: keep cursor on subject column after j/k --- (defun my/mu4e-goto-subject (&rest _) "Move cursor to the start of the subject text in a headers line." (when (derived-mode-p 'mu4e-headers-mode) (let* ((msg (mu4e-message-at-point t)) (subject (when msg (mu4e-message-field msg :subject)))) (when (and subject (> (length subject) 0)) (beginning-of-line) (let ((needle (substring subject 0 (min 10 (length subject))))) (when (search-forward needle (line-end-position) t) (goto-char (match-beginning 0)))))))) (advice-add 'mu4e-headers-next :after #'my/mu4e-goto-subject) (advice-add 'mu4e-headers-prev :after #'my/mu4e-goto-subject) ;; zT = toggle thread view (plain T marks the thread for bulk action) (evil-define-key 'normal mu4e-headers-mode-map (kbd "zT") #'mu4e-headers-toggle-threading)) ;;; ============================================================ ;;; RSS — ELFEED ;;; ============================================================ (after! org (setq rmh-elfeed-org-files (list (expand-file-name "elfeed.org" org-directory)))) (after! elfeed (require 'elfeed-org) (elfeed-org)) ;;; ============================================================ ;;; 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) "Strip 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 TYPE (png or svg)." (interactive (list (completing-read "Type: " '("png" "svg") nil t "png"))) (unless buffer-file-name (user-error "Open .puml as a 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")))) ;;; ============================================================ ;;; TRAMP & REMOTE ;;; ============================================================ (after! tramp (setq projectile-git-command "git ls-files -zco --exclude-standard")) ;; Disable VC and Projectile over TRAMP — main cause of hangs (setq vc-ignore-dir-regexp (format "%s\\|%s" vc-ignore-dir-regexp tramp-file-name-regexp)) (advice-add 'projectile-project-root :around (lambda (orig-fn &rest args) (unless (file-remote-p default-directory) (apply orig-fn args)))) (setq remote-file-name-inhibit-cache nil tramp-verbose 1) ;;; ============================================================ ;;; DIRED & DIRVISH ;;; ============================================================ ;; Always hide file details (permissions, size, date) for VoiceOver. ;; Toggle visibility with ( in dired/dirvish buffers. ;; Three layers of insurance: dirvish-hide-details, dired-mode-hook, ;; and dired-after-readin-hook (catches late buffer setup). (add-hook 'dired-mode-hook #'dired-hide-details-mode) ;; Attach marked files to mail compose buffer via C-c RET C-a (add-hook 'dired-mode-hook #'turn-on-gnus-dired-mode) (add-hook 'dired-after-readin-hook (lambda () (unless dired-hide-details-mode (dired-hide-details-mode 1)))) ;; Emacs 31 may not autoload dired-read-dir-and-switches early enough (require 'dired) ;; macOS: use GNU ls (coreutils) for dired/dirvish sorting support (setq insert-directory-program "gls") (after! dired (put 'dired-find-alternate-file 'disabled nil) (map! :map dired-mode-map "RET" #'dired-find-alternate-file "^" #'dired-up-directory)) ;; Dirvish — modern dired replacement (use-package! dirvish :init (dirvish-override-dired-mode) :config (setq dirvish-hide-details t dirvish-mode-line-format '(:left (sort symlink) :right (omit yank index)) dirvish-attributes '(vc-state subtree-state nerd-icons collapse git-msg) dirvish-side-width 35) (defun my/dirvish-toggle-details () "Toggle file-time and file-size dirvish attributes." (interactive) (if (memq 'file-size dirvish-attributes) (setq-local dirvish-attributes (seq-remove (lambda (a) (memq a '(file-time file-size))) dirvish-attributes)) (setq-local dirvish-attributes (append dirvish-attributes '(file-time file-size)))) (revert-buffer)) (map! :map dirvish-mode-map :n "D" #'my/dirvish-toggle-details :n "q" #'dirvish-quit :n "h" #'dired-up-directory :n "l" #'dired-find-file :n "s" #'dirvish-quicksort :n "S" #'dirvish-setup-menu :n "TAB" #'dirvish-subtree-toggle :n "M-f" #'dirvish-history-go-forward :n "M-b" #'dirvish-history-go-backward)) ;;; ============================================================ ;;; 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")) (dolist (dir '("node_modules" "__pycache__" ".terraform" "vendor")) (add-to-list 'projectile-globally-ignored-directories dir))) ;;; ============================================================ ;;; PYTHON ;;; ============================================================ (setq python-shell-interpreter "python3") (after! org (setq org-babel-python-command "python3") (require 'ob-python)) (after! python (map! :map python-mode-map :localleader :desc "Format (ruff)" "f" (cmd! (compile (concat "ruff format " (buffer-file-name)) (revert-buffer t t t))) :desc "Run file" "r" (cmd! (compile (concat "python3 " (buffer-file-name)))))) ;;; ============================================================ ;;; DEVELOPER WORKFLOW — Perl, Go, Ansible, Terraform, Docker ;;; ============================================================ ;;; --- Perl (cperl-mode) --- (add-to-list 'auto-mode-alist '("\\.pl\\'" . cperl-mode)) (add-to-list 'auto-mode-alist '("\\.pm\\'" . cperl-mode)) (add-to-list 'auto-mode-alist '("\\.t\\'" . cperl-mode)) (after! cperl-mode (setq cperl-indent-level 4 cperl-close-paren-offset -4 cperl-continued-statement-offset 4 cperl-indent-parens-as-block t cperl-tab-always-indent t)) (use-package! reformatter :config (reformatter-define perl-tidy :program "perltidy" :args '("-st" "-se" "--quiet"))) (after! cperl-mode (map! :map cperl-mode-map :localleader :desc "Format (perltidy)" "f" #'perl-tidy-buffer :desc "Run file" "r" (cmd! (compile (concat "perl " (buffer-file-name)))) :desc "Debug (perldb)" "d" #'perldb) (add-hook 'cperl-mode-hook (lambda () (when (fboundp 'flycheck-select-checker) (flycheck-select-checker 'perl))))) ;;; --- Go --- (after! go-mode (setq gofmt-command "goimports") (add-hook 'go-mode-hook (lambda () (add-hook 'before-save-hook #'gofmt-before-save nil t))) (map! :map go-mode-map :localleader :desc "Format (goimports)" "f" #'gofmt :desc "Run file" "r" (cmd! (compile (concat "go run " (buffer-file-name)))) :desc "Test" "t" (cmd! (compile "go test ./...")) :desc "Build" "b" (cmd! (compile "go build ./...")))) ;;; --- Ansible / YAML --- (after! yaml-mode (map! :map yaml-mode-map :localleader :desc "Run playbook" "r" (cmd! (compile (concat "ansible-playbook " (buffer-file-name)))))) (after! flycheck (flycheck-define-checker yaml-ansible-lint "Ansible linter." :command ("ansible-lint" "--parseable" source) :error-patterns ((warning line-start (file-name) ":" line ": " (message) line-end)) :modes (yaml-mode)) (add-to-list 'flycheck-checkers 'yaml-ansible-lint t)) ;;; --- Terraform --- (after! terraform-mode (map! :map terraform-mode-map :localleader :desc "Format (terraform fmt)" "f" (cmd! (compile "terraform fmt" t) (revert-buffer t t t)) :desc "Init" "i" (cmd! (compile "terraform init")) :desc "Plan" "p" (cmd! (compile "terraform plan")))) ;;; --- Dockerfile / Podman --- (after! dockerfile-mode (map! :map dockerfile-mode-map :localleader :desc "Build (podman)" "b" (cmd! (let ((tag (read-string "Image tag: " "myapp:latest"))) (compile (format "podman build -t %s -f %s ." tag (buffer-file-name))))) :desc "Run (podman)" "r" (cmd! (let ((img (read-string "Image name: " "myapp:latest"))) (compile (format "podman run --rm %s" img)))))) (after! flycheck (when (executable-find "hadolint") (flycheck-define-checker dockerfile-hadolint "Dockerfile linter using hadolint." :command ("hadolint" "--no-color" "-") :standard-input t :error-patterns ((info line-start (one-or-more not-newline) ":" line " " (id (one-or-more not-newline)) " " (message) line-end) (warning line-start (one-or-more not-newline) ":" line " " (id (one-or-more not-newline)) " " (message) line-end) (error line-start (one-or-more not-newline) ":" line " " (id (one-or-more not-newline)) " " (message) line-end)) :modes (dockerfile-mode)) (add-to-list 'flycheck-checkers 'dockerfile-hadolint t))) ;;; ============================================================ ;;; ACCESSIBILITY — EMACSPEAK ;;; ============================================================ ;;; Default: OFF. Toggle with SPC t s (on) / SPC t S (off). (defconst my/emacspeak-dir (expand-file-name "~/.emacspeak")) (defconst my/emacspeak-wrapper (expand-file-name "~/.local/bin/emacspeak-mac")) (setq dtk-program my/emacspeak-wrapper) (defvar my/emacspeak-loaded nil) (defvar my/emacspeak-enabled nil) (defvar my/emacspeak-inhibit-server t "When non-nil, Emacspeak server will not start/restart.") (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)) (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 (apply orig args))))))))) (defun my/emacspeak-on () "Enable speech and allow TTS 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 and prevent auto-restart." (interactive) (setq my/emacspeak-enabled nil 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)")) (with-eval-after-load 'dtk-speak (setq dtk-speech-rate-base 300) (setq-default dtk-punctuation-mode 'none)) (with-eval-after-load 'emacspeak (setq-default emacspeak-character-echo nil emacspeak-word-echo t emacspeak-line-echo t)) (setq dtk-default-speech-rate 400) (with-eval-after-load 'dtk-speak (defun my/dtk-apply-global-default-rate (&rest _) "Apply global default speech rate after TTS init/restart." (when (fboundp 'dtk-set-rate) (ignore-errors (dtk-set-rate dtk-default-speech-rate t)))) (advice-add 'dtk-initialize :after #'my/dtk-apply-global-default-rate)) ;;; ============================================================ ;;; ACCESSIBILITY — GLOBAL ZOOM (SPC z) ;;; ============================================================ ;; Scales the global `default' face — all buffers, help, menus included. ;; Modeline + header-line pinned to base size. Step: x1.5 per step. ;; SPC z +/= zoom in, SPC z - zoom out, SPC z 0 reset, SPC z z restore. (defvar my/zoom-base-height 140) (defvar my/zoom-steps 0) (defvar my/zoom-saved-steps nil) (defvar my/zoom-pinned-faces '(mode-line mode-line-inactive mode-line-active header-line tab-bar tab-bar-tab tab-bar-tab-inactive)) (defun my/zoom-pin-ui () "Set all pinned UI faces to base height." (dolist (face my/zoom-pinned-faces) (when (facep face) (set-face-attribute face nil :height my/zoom-base-height)))) (after! which-key (setq which-key-side-window-max-height 0.90 which-key-max-display-columns nil)) (defun my/zoom--apply (steps) "Scale global default face to base x 1.5^STEPS and re-pin UI faces." (let ((new-h (max 80 (round (* my/zoom-base-height (expt 1.5 steps)))))) (set-face-attribute 'default nil :height new-h) (my/zoom-pin-ui) (when (fboundp 'corfu--popup-hide) (ignore-errors (corfu--popup-hide))) (message "Zoom %+d x%.2f ~%dpt" steps (expt 1.5 steps) (/ new-h 10)) (when (and (not (minibufferp)) (window-live-p (selected-window))) (scroll-right (window-hscroll)) (recenter nil)))) (add-hook 'doom-after-init-hook (lambda () (let ((h (face-attribute 'default :height nil t))) (when (and (integerp h) (> h 0)) (setq my/zoom-base-height h))))) (add-hook 'doom-load-theme-hook #'my/zoom-pin-ui) (defun my/zoom-in () (interactive) (cl-incf my/zoom-steps) (my/zoom--apply my/zoom-steps)) (defun my/zoom-out () (interactive) (cl-decf my/zoom-steps) (my/zoom--apply my/zoom-steps)) (defun my/zoom-reset () "Reset to default font size. Saves current level for restore." (interactive) (if (= my/zoom-steps 0) (message "Zoom: already at default") (setq my/zoom-saved-steps my/zoom-steps) (my/zoom--apply 0) (setq my/zoom-steps 0) (message "Zoom reset (SPC z z to restore %+d)" my/zoom-saved-steps))) (defun my/zoom-restore () "Restore zoom level saved before last reset." (interactive) (if (null my/zoom-saved-steps) (message "Zoom: nothing to restore") (my/zoom--apply my/zoom-saved-steps) (setq my/zoom-steps my/zoom-saved-steps my/zoom-saved-steps nil))) (setq hscroll-margin 3 hscroll-step 1 scroll-conservatively 101 scroll-margin 2) ;;; ============================================================ ;;; MATRIX — EMENT.EL ;;; ============================================================ ;; Matrix client. SPC o M (open -> matrix). ;; SPC o m is taken by Doom's mu4e module, hence uppercase M. (setq ement-save-sessions t ement-sessions-file (expand-file-name "ement-sessions.el" doom-private-dir)) (after! ement (setq ement-auto-sync t ement-room-timestamp-format "%H:%M" ement-room-show-avatars nil ement-room-username-display-property '(raise 0) ement-notify-mentions-p t ement-notify-dingalings-p nil)) (defun my/ement-open-after-sync (&rest _) "Open room list after ement finishes initial sync. Self-removing." (remove-hook 'ement-after-initial-sync-hook #'my/ement-open-after-sync) (when (fboundp 'ement-list-rooms) (ement-list-rooms))) (defun my/ement-maybe-restore () "Restore saved ement session silently on startup." (require 'ement) (condition-case err (let ((sf (expand-file-name ement-sessions-file))) (when (file-readable-p sf) (unless ement-sessions (when (fboundp 'ement--load-sessions) (setq ement-sessions (ement--load-sessions)))) (if (fboundp 'ement--reconnect) (dolist (entry ement-sessions) (ement--reconnect (cdr entry))) (when ement-sessions (ement-connect :user-id (caar ement-sessions) :homeserver "https://matrix.apps.sukany.cz"))))) (error (message "ement startup restore: %s" (error-message-string err))))) (defun my/ement-open () "Open Matrix panel: show room list (connect/restore if needed)." (interactive) (require 'ement) (cond ((and (boundp 'ement-sessions) ement-sessions) (ement-list-rooms)) ((file-readable-p (expand-file-name ement-sessions-file)) (add-hook 'ement-after-initial-sync-hook #'my/ement-open-after-sync) (my/ement-maybe-restore)) (t (add-hook 'ement-after-initial-sync-hook #'my/ement-open-after-sync) (call-interactively #'ement-connect)))) (add-hook 'doom-after-init-hook #'my/ement-maybe-restore) (map! :leader (:prefix ("o M" . "Matrix") :desc "Open panel" "o" #'my/ement-open :desc "Connect" "c" #'ement-connect :desc "Disconnect" "C" #'ement-disconnect :desc "List rooms" "l" #'ement-list-rooms :desc "Open room" "r" #'ement-view-room :desc "Direct message" "d" #'ement-send-direct-message :desc "Join room" "j" #'ement-join-room :desc "Notifications" "n" #'ement-notifications :desc "Mentions" "m" #'ement-mentions)) ;;; ============================================================ ;;; PDF TOOLS ;;; ============================================================ (after! pdf-tools (pdf-tools-install :no-query)) (after! pdf-view (setq-default pdf-view-display-size 'fit-page) (setq pdf-view-use-scaling t pdf-view-use-imagemagick nil pdf-view-midnight-colors '("#d4d4d4" . "#1c1c1c") pdf-view-continuous t) (add-hook 'pdf-view-mode-hook (lambda () (display-line-numbers-mode -1))) (add-hook 'pdf-view-mode-hook #'auto-revert-mode)) (after! pdf-view (evil-define-key 'normal pdf-view-mode-map "j" #'pdf-view-next-line-or-next-page "k" #'pdf-view-previous-line-or-previous-page "J" #'pdf-view-next-page "K" #'pdf-view-previous-page "gg" #'pdf-view-first-page "G" #'pdf-view-last-page "+" #'pdf-view-enlarge "-" #'pdf-view-shrink "=" #'pdf-view-fit-page-to-window "W" #'pdf-view-fit-width-to-window "/" #'isearch-forward "?" #'isearch-backward "m" #'pdf-view-midnight-minor-mode "a" #'pdf-annot-add-highlight-markup-annotation "q" #'quit-window)) ;; Re-activate Evil normal state when switching to a pdf-view window (defun my/pdf-view-ensure-normal-state (&rest _) "Activate evil-normal-state when landing on a pdf-view-mode buffer." (when (and (derived-mode-p 'pdf-view-mode) (called-interactively-p 'any)) (evil-normal-state))) (advice-add 'evil-window-next :after #'my/pdf-view-ensure-normal-state) (advice-add 'evil-window-prev :after #'my/pdf-view-ensure-normal-state) (advice-add 'other-window :after #'my/pdf-view-ensure-normal-state) ;; Open PDFs from org export in Emacs instead of Preview.app (when (eq system-type 'darwin) (setq org-file-apps '((auto-mode . emacs) (directory . emacs) ("\\.mm\\'" . default) ("\\.x?html?\\'" . default) ("\\.pdf\\'" . emacs)))) ;;; ============================================================ ;;; YASNIPPET — snippets from ~/org/snippets ;;; ============================================================ (after! yasnippet (push (expand-file-name "snippets/" org-directory) yas-snippet-dirs) (yas-reload-all) ;; Select default text when entering a snippet field in Evil (add-hook 'yas-before-expand-snippet-hook (lambda () (when (evil-normal-state-p) (evil-insert-state)))) (add-hook 'yas-keymap-disable-hook (lambda () (not (yas--snippets-at-point)))) (define-key yas-keymap (kbd "C-d") (lambda () (interactive) (delete-region (yas-field-start (yas-current-field)) (yas-field-end (yas-current-field)))))) ;;; ============================================================ ;;; NAVIGATION — link-hint, avy ;;; ============================================================ (use-package! link-hint :defer t :commands (link-hint-open-link link-hint-copy-link)) (map! :leader (:prefix ("j" . "jump") :desc "Open link (link-hint)" "k" #'link-hint-open-link :desc "Copy link URL" "K" #'link-hint-copy-link :desc "Avy goto char-2" "j" #'avy-goto-char-2 :desc "Avy goto line" "l" #'avy-goto-line)) ;;; ============================================================ ;;; WRITING — olivetti-mode ;;; ============================================================ (after! olivetti (setq olivetti-body-width 90)) (add-hook 'org-mode-hook (lambda () (when buffer-file-name (olivetti-mode 1)))) (add-hook 'markdown-mode-hook (lambda () (when buffer-file-name (olivetti-mode 1)))) (add-hook 'text-mode-hook (lambda () (when buffer-file-name (olivetti-mode 1)))) (map! :leader (:prefix ("t" . "toggle") :desc "Olivetti mode" "o" #'olivetti-mode)) ;;; ============================================================ ;;; ORG-MODERN ;;; ============================================================ (use-package! org-modern :hook (org-mode . org-modern-mode) :hook (org-agenda-finalize . org-modern-agenda) :config (setq org-modern-star ["◉"] org-modern-hide-stars "·" org-modern-table nil)) ;;; ============================================================ ;;; ORG-FRAGTOG — auto-render LaTeX fragments ;;; ============================================================ (use-package! org-fragtog :after org :hook (org-mode . my/org-fragtog-maybe)) (defun my/org-fragtog-maybe () "Enable org-fragtog-mode only in file-backed buffers." (when buffer-file-name (org-fragtog-mode 1))) ;;; ============================================================ ;;; ORG-SUPER-AGENDA ;;; ============================================================ (use-package! org-super-agenda :after org-agenda :config ;; Agenda: start from today, no past days, no duplicates (setq org-agenda-start-on-weekday nil org-agenda-start-day "0d" org-agenda-span 7 org-agenda-skip-scheduled-if-done t org-agenda-skip-deadline-if-done t org-agenda-skip-scheduled-if-deadline-is-shown t org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled) ;; Sorting: priority first, then deadline, then scheduled (setq org-agenda-sorting-strategy '((agenda priority-down deadline-up scheduled-up) (todo priority-down deadline-up) (tags priority-down deadline-up))) (setq org-super-agenda-groups '((:name "Overdue" :deadline past) (:name "Due today" :deadline today) (:name "Scheduled today" :scheduled today) (:name "Due soon" :deadline future) (:name "Waiting" :todo "WAIT") (:name "Kyndryl" :tag ("kyndryl" "work")) (:name "ZTJ" :tag "ztj") (:name "Other" :anything t)))) (after! org-super-agenda (org-super-agenda-mode 1) ;; Fix: org-super-agenda applies its own keymap to group headers ;; via text properties, overriding org-agenda keybindings. ;; Setting it to nil disables the header keymap entirely so all ;; standard agenda keys (m, B, t, etc.) work on every line. (setq org-super-agenda-header-map nil)) ;;; ============================================================ ;;; ORG-NOTER — PDF annotations ;;; ============================================================ (use-package! org-noter :after (:any org pdf-view) :config (setq org-noter-notes-window-location 'horizontal-split org-noter-notes-search-path (list (expand-file-name "annotations/" org-directory)) org-noter-default-notes-file-names '("notes.org") org-noter-auto-save-last-location t org-noter-insert-note-no-questions nil org-noter-use-indirect-buffer nil org-noter-always-create-frame nil)) ;; Smart org-noter launcher: repairs broken notes files and starts from PDF window (defun my/org-noter-start () "Start org-noter for the PDF visible in the current frame." (interactive) (require 'org-noter) (let* ((pdf-win (if (derived-mode-p 'pdf-view-mode) (selected-window) (cl-find-if (lambda (w) (with-current-buffer (window-buffer w) (derived-mode-p 'pdf-view-mode))) (window-list (selected-frame))))) (pdf-path (when pdf-win (with-current-buffer (window-buffer pdf-win) (buffer-file-name))))) (unless pdf-path (user-error "No PDF buffer visible — open a PDF first")) (let* ((base (file-name-base pdf-path)) (notes-name (concat base ".org")) (notes-dir (car org-noter-notes-search-path)) (target (expand-file-name notes-name notes-dir))) (make-directory notes-dir t) ;; Repair or create notes file with correct NOTER_DOCUMENT path (with-current-buffer (find-file-noselect target) (let ((modified nil)) (save-excursion (goto-char (point-min)) (if (re-search-forward (concat "^[ \t]*:" org-noter-property-doc-file ":[ \t]*\\(.*\\)$") nil t) (let* ((stored (string-trim (match-string 1))) (expanded (if (file-name-absolute-p stored) stored (expand-file-name stored (file-name-directory target))))) (unless (and (file-exists-p expanded) (file-equal-p expanded pdf-path)) (replace-match (concat ":" org-noter-property-doc-file ": " pdf-path) t t) (setq modified t))) (goto-char (point-min)) (insert (format "* Notes: %s :noexport:\n:PROPERTIES:\n:%s: %s\n:END:\n\n" base org-noter-property-doc-file pdf-path)) (setq modified t))) (when modified (save-buffer)))) ;; Start from the PDF window for proper frame split (cl-letf* ((orig-cr (symbol-function 'completing-read)) ((symbol-function 'completing-read) (lambda (prompt collection &rest args) (cond ((string-match-p "name" prompt) notes-name) ((string-match-p "save\\|where\\|Which" prompt) target) (t (apply orig-cr prompt collection args)))))) (with-selected-window pdf-win (org-noter)))))) (map! :leader (:prefix ("o" . "open") :desc "org-noter" "n" #'my/org-noter-start :desc "org-noter insert note" "N" #'org-noter-insert-note)) ;;; ============================================================ ;;; ORG-CALDAV — CalDAV sync (4 calendars) ;;; ============================================================ ;; Baikal server: cal.apps.sukany.cz ;; Credentials: ~/.authinfo (machine cal.apps.sukany.cz login martin password ...) ;; ;; Files: ;; ~/org/calendar_outbox.org — events to upload (in agenda-files) ;; ~/org/caldav/suky.org — downloaded from Suky calendar (NOT in agenda) ;; ~/org/caldav/placeholders.org — Placeholders calendar (NOT in agenda) ;; ~/org/caldav/family.org — Family calendar (NOT in agenda) ;; ~/org/caldav/klara.org — Klara's personal calendar (NOT in agenda) ;; ;; caldav/ files are kept out of agenda to avoid polluting it with ;; historical events. View them via calfw (SPC o C). (use-package! org-caldav :commands my/org-caldav-sync :config (setq org-caldav-days-in-past 1825 ; 5 years back org-caldav-delete-org-entries 'never org-caldav-delete-calendar-entries 'never org-caldav-save-directory "~/org/") ;; Error handler: catch errors during cal->org event update ;; so sync state is saved even if individual events fail (advice-add 'org-caldav-update-events-in-org :around (lambda (orig-fn &rest args) "Catch errors during cal->org sync; log and return so sync state is saved." (condition-case err (apply orig-fn args) (error (message "org-caldav: update-events-in-org error (sync continues): %S" err) (org-caldav-debug-print 1 (format "update-events-in-org error: %S" err)))))) (defun my/org-caldav-sync () "Sync 4 CalDAV calendars: Suky (twoway), Placeholders, Family, Klara (read-only)." (interactive) ;; Ensure caldav/ directory and inbox files exist (make-directory "~/org/caldav" t) (dolist (f '("~/org/caldav/suky.org" "~/org/caldav/placeholders.org" "~/org/caldav/family.org" "~/org/caldav/klara.org")) (unless (file-exists-p (expand-file-name f)) (with-temp-file (expand-file-name f) (insert "#+TITLE: CalDAV sync\n#+STARTUP: overview\n")))) ;; 1. Suky (twoway): download -> suky.org, upload from calendar_outbox.org (setq org-caldav-url my/caldav-url org-caldav-calendar-id my/caldav-id-suky org-caldav-inbox "~/org/caldav/suky.org" org-caldav-files '("~/org/calendar_outbox.org") org-caldav-sync-direction 'twoway) (org-caldav-sync) ;; 2. Placeholders (read-only) (setq org-caldav-url my/caldav-url org-caldav-calendar-id my/caldav-id-placeholders org-caldav-inbox "~/org/caldav/placeholders.org" org-caldav-files nil org-caldav-sync-direction 'cal->org) (org-caldav-sync) ;; 3. Family (read-only, shared via Baikal ACL) (setq org-caldav-url my/caldav-url org-caldav-calendar-id my/caldav-id-family org-caldav-inbox "~/org/caldav/family.org" org-caldav-files nil org-caldav-sync-direction 'cal->org) (org-caldav-sync) ;; 4. Klara (read-only, shared via Baikal ACL) (setq org-caldav-url my/caldav-url org-caldav-calendar-id my/caldav-id-klara org-caldav-inbox "~/org/caldav/klara.org" org-caldav-files nil org-caldav-sync-direction 'cal->org) (org-caldav-sync) (message "CalDAV sync done: Suky + Placeholders + Family + Klara"))) ;;; ============================================================ ;;; CALFW — visual calendar ;;; ============================================================ (use-package! calfw :demand t) (use-package! calfw-org :demand t :config ;; org-capture integration: "a" to add event on selected date ;; Store date from calfw before capture starts (defvar my/calfw-capture-date nil "Date selected in calfw for capture.") (defun my/calfw-capture () "Start org-capture with calfw date context." (interactive) (setq my/calfw-capture-date (calfw-cursor-to-nearest-date)) (org-capture nil "c")) ;; Prompts for start/end time; empty = all-day event ;; Uses active timestamp with time range for timed events, ;; or date-range (-->--) for multi-day events. (defun my/calfw-capture-timestamp () "Build org timestamp for selected calfw date with optional time prompt. Formats matching what org-caldav/ox-icalendar export correctly: - All-day single: <2026-02-26 Thu> - Timed single day: <2026-02-26 Thu 14:00-15:30> - All-day multi: <2026-02-26 Thu>--<2026-02-28 Sat> - Timed multi-day: <2026-02-26 Thu 15:00>--<2026-02-28 Sat 22:00>" (let* ((date (or my/calfw-capture-date (calendar-current-date))) (m (nth 0 date)) (d (nth 1 date)) (y (nth 2 date)) (dow (format-time-string "%a" (encode-time 0 0 12 d m y))) (start-time (read-string "Start time (HH:MM, empty=all-day): ")) (end-input (unless (string-empty-p start-time) (read-string "End time or end date (HH:MM / YYYY-MM-DD / YYYY-MM-DD HH:MM, empty=same day open): ")))) (cond ;; All-day event (no time given) ((string-empty-p start-time) (let ((end-date (read-string "End date for multi-day (YYYY-MM-DD, empty=single day): "))) (if (string-empty-p end-date) (format "<%04d-%02d-%02d %s>" y m d dow) (let* ((parsed (parse-time-string (concat end-date " 00:00"))) (ed (nth 3 parsed)) (em (nth 4 parsed)) (ey (nth 5 parsed)) (edow (format-time-string "%a" (encode-time 0 0 12 ed em ey)))) (format "<%04d-%02d-%02d %s>--<%04d-%02d-%02d %s>" y m d dow ey em ed edow))))) ;; Timed event - check if end contains a date (multi-day) ((and end-input (not (string-empty-p end-input)) (string-match "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}" end-input)) ;; Multi-day with times (let* ((end-time-part (if (string-match " \\([0-9]\\{2\\}:[0-9]\\{2\\}\\)" end-input) (match-string 1 end-input) "")) (end-date-part (substring end-input 0 10)) (parsed (parse-time-string (concat end-date-part " 00:00"))) (ed (nth 3 parsed)) (em (nth 4 parsed)) (ey (nth 5 parsed)) (edow (format-time-string "%a" (encode-time 0 0 12 ed em ey)))) (if (string-empty-p end-time-part) (format "<%04d-%02d-%02d %s %s>--<%04d-%02d-%02d %s>" y m d dow start-time ey em ed edow) (format "<%04d-%02d-%02d %s %s>--<%04d-%02d-%02d %s %s>" y m d dow start-time ey em ed edow end-time-part)))) ;; Same-day timed event with end time (HH:MM) ((and end-input (not (string-empty-p end-input))) (format "<%04d-%02d-%02d %s %s-%s>" y m d dow start-time end-input)) ;; Same-day timed event, no end time (t (format "<%04d-%02d-%02d %s %s>" y m d dow start-time))))) (setq calfw-org-capture-template '("c" "Calendar event" entry (file "~/org/calendar_outbox.org") "* %?\n%(my/calfw-capture-timestamp)\n")) ;; Evil normal state for calfw (SPC = Doom leader works) (evil-set-initial-state 'calfw-calendar-mode 'normal) (evil-set-initial-state 'calfw-details-mode 'normal) (evil-define-key 'normal calfw-calendar-mode-map "h" #'calfw-navi-previous-day-command "l" #'calfw-navi-next-day-command "j" #'calfw-navi-next-week-command "k" #'calfw-navi-previous-week-command "n" #'calfw-navi-next-week-command "p" #'calfw-navi-previous-week-command "t" #'calfw-navi-goto-today-command "g" #'calfw-refresh-calendar-buffer "W" #'calfw-change-view-week "M" #'calfw-change-view-month "D" #'calfw-change-view-day "TAB" #'calfw-navi-next-item-command "a" #'my/calfw-capture "q" #'bury-buffer "x" #'calfw-org-clean-exit "RET" #'calfw-org-open-agenda-day "<" #'calfw-navi-prev-view ">" #'calfw-navi-next-view) ;; Sort periods (same-day events from org-caldav active-range format) (defun my/calfw-event-time-int (evt) "Return start-time as HHMM integer, or nil." (when-let ((st (calfw-event-start-time evt))) (+ (* 100 (car st)) (cadr st)))) (defun my/calfw-sort-periods (result) "Sort periods sublist in RESULT by start-time." (mapcar (lambda (item) (if (and (listp item) (eq 'periods (car item))) (cons 'periods (sort (copy-sequence (cdr item)) (lambda (a b) (let ((ta (my/calfw-event-time-int a)) (tb (my/calfw-event-time-int b))) (cond ((and ta tb) (< ta tb)) (ta t) (t nil)))))) item)) result)) (defun my/calfw-sorted-file-source (name file color) "Create calfw-org file source with sorted periods." (let ((base (calfw-org-create-file-source name file color))) (make-calfw-source :name (calfw-source-name base) :color (calfw-source-color base) :data (lambda (begin end) (my/calfw-sort-periods (funcall (calfw-source-data base) begin end)))))) (defun my/calfw-sorter (x y) (let* ((ex (get-text-property 0 'cfw:event x)) (ey (get-text-property 0 'cfw:event y)) (ta (or (and ex (my/calfw-event-time-int ex)) (get-text-property 0 'time-of-day x))) (tb (or (and ey (my/calfw-event-time-int ey)) (get-text-property 0 'time-of-day y)))) (cond ((and ta tb) (< ta tb)) (ta t) (tb nil) (t (string-lessp x y))))) (defun my/open-calendar () "Open calfw with org-agenda + CalDAV sources (Suky, Klara, Family)." (interactive) (condition-case err (let* ((cd (expand-file-name "~/org/caldav/")) (sources (delq nil (list (calfw-org-create-source nil "Agenda" "SeaGreen4") (when (file-exists-p (concat cd "suky.org")) (my/calfw-sorted-file-source "Suky" (concat cd "suky.org") "SteelBlue")) (when (file-exists-p (concat cd "klara.org")) (my/calfw-sorted-file-source "Klara" (concat cd "klara.org") "Gold")) (when (file-exists-p (concat cd "family.org")) (my/calfw-sorted-file-source "Family" (concat cd "family.org") "ForestGreen")))))) (calfw-open-calendar-buffer :contents-sources sources :view 'month :custom-map calfw-org-custom-map :sorter #'my/calfw-sorter)) (error (message "calfw multi-source: %s — fallback" (error-message-string err)) (calfw-org-open-calendar)))) (map! :leader "o C" #'my/open-calendar)) ;;; ============================================================ ;;; EXTENSIONS — envrc, embark, wgrep, kubel ;;; ============================================================ (use-package! envrc :hook (after-init . envrc-global-mode)) (after! embark (setq embark-prompter #'embark-keymap-prompter) (map! "C-." #'embark-act (:map minibuffer-local-map "C-." #'embark-act))) (after! wgrep (setq wgrep-auto-save-buffer t)) (use-package! kubel :commands kubel :config (map! :map kubel-mode-map :n "g" #'kubel-get-resource-details :n "E" #'kubel-exec-pod :n "l" #'kubel-get-pod-logs :n "d" #'kubel-describe-resource :n "D" #'kubel-delete-resource)) ;;; ============================================================ ;;; ORG-CLOCK — time tracking ;;; ============================================================ (after! org (setq org-clock-persist 'history org-clock-in-resume t org-clock-out-remove-zero-time-clocks t org-clock-report-include-clocking-task t org-duration-format 'h:mm) (org-clock-persistence-insinuate) (map! :map org-mode-map :localleader "C i" #'org-clock-in "C o" #'org-clock-out "C r" #'org-clock-report "C d" #'org-clock-display)) ;;; ============================================================ ;;; COMBOBULATE — tree-sitter structural editing ;;; ============================================================ (use-package! combobulate :hook ((python-mode . combobulate-mode) (python-ts-mode . combobulate-mode) (go-mode . combobulate-mode) (go-ts-mode . combobulate-mode) (js-mode . combobulate-mode) (typescript-mode . combobulate-mode)) :config (map! :map combobulate-mode-map :n "C-M-n" #'combobulate-navigate-next :n "C-M-p" #'combobulate-navigate-previous :n "C-M-u" #'combobulate-navigate-up :n "C-M-d" #'combobulate-navigate-down)) ;;; ============================================================ ;;; MISC EXTENSIONS — iedit, vundo, breadcrumb ;;; ============================================================ (use-package! iedit :commands iedit-mode) (use-package! vundo :commands vundo :config (setq vundo-glyph-alist vundo-unicode-symbols)) (use-package! breadcrumb :hook ((prog-mode . breadcrumb-local-mode) (cperl-mode . breadcrumb-local-mode))) ;;; ============================================================ ;;; EVIL — ORG TABLE CELL TEXT OBJECTS (di| ci| vi|) ;;; ============================================================ (after! evil-org (evil-org-set-key-theme '(navigation insert textobjects additional calendar)) (evil-define-text-object evil-org-inner-table-cell (count &optional beg end type) "Inner org table cell (content between pipes)." (when (org-at-table-p) (let ((b (save-excursion (search-backward "|") (forward-char 1) (skip-chars-forward " ") (point))) (e (save-excursion (search-forward "|") (backward-char 1) (skip-chars-backward " ") (point)))) (list b e)))) (define-key evil-inner-text-objects-map "|" #'evil-org-inner-table-cell) (define-key evil-outer-text-objects-map "|" #'evil-org-inner-table-cell)) ;;; ============================================================ ;;; GIT — git-link, Forge (Gitea) ;;; ============================================================ (use-package! git-link :defer t :config (setq git-link-default-branch "master") (add-to-list 'git-link-remote-alist '("git\\.apps\\.sukany\\.cz" git-link-gitea)) (add-to-list 'git-link-commit-remote-alist '("git\\.apps\\.sukany\\.cz" git-link-commit-gitea))) (map! :leader (:prefix ("g" . "git") :desc "Copy git link" "y" #'git-link :desc "Copy git link commit" "Y" #'git-link-commit)) (after! forge (add-to-list 'forge-alist '("git.apps.sukany.cz" "git.apps.sukany.cz/api/v1" "git.apps.sukany.cz" forge-gitea-repository))) ;;; ============================================================ ;;; BIBLIOGRAPHY — citar + biblio + org-cite ;;; ============================================================ ;; Global .bib: ~/org/references.bib (always available) ;; Per-project: add #+BIBLIOGRAPHY: ./refs.bib in org file header ;; ;; Workflow: ;; SPC B n → create new .bib (prompted for path) ;; SPC B s → search CrossRef → in results press 'i' to save to .bib ;; SPC B i → insert citation in org file ;; Export: org-cite + CSL auto-formats references ;; ;; Per-project setup: put .dir-locals.el in project root: ;; ((org-mode . ((citar-bibliography . ("./refs.bib"))))) ;; Global bibliography paths (setq! required for Doom's custom setter) (after! citar (setq! citar-bibliography '("~/org/references.bib")) (setq! citar-notes-paths '("~/org/notes/")) (setq! citar-library-paths '("~/org/papers/")) ;; Auto-create global .bib if missing (let ((bib (expand-file-name "~/org/references.bib"))) (unless (file-exists-p bib) (make-directory (file-name-directory bib) t) (with-temp-file bib (insert "% Global bibliography\n\n"))))) ;; org-cite: CSL processor (no BibLaTeX toolchain needed) (after! oc (require 'citeproc) (setq org-cite-global-bibliography '("~/org/references.bib") org-cite-export-processors '((latex csl) (html csl) (t csl)))) ;; biblio.el: polite CrossRef access (required by their API policy) (setq biblio-crossref-user-email-address "martin@sukany.cz") ;; BibTeX editing defaults (setq bibtex-dialect 'biblatex bibtex-autokey-year-length 4 bibtex-autokey-name-year-separator "" bibtex-autokey-year-title-separator "-" bibtex-autokey-titleword-length 5 bibtex-autokey-titlewords 3) ;; Helper: create new .bib file for a project (defun my/bib-new () "Create a new .bib file at prompted path and open it." (interactive) (let ((path (read-file-name "New .bib file: " default-directory nil nil "refs.bib"))) (make-directory (file-name-directory path) t) (unless (file-exists-p path) (with-temp-file path (insert (format "%% Bibliography: %s\n%% Add entries via SPC B s (search) or SPC B m (manual)\n\n" (file-name-nondirectory path))))) (find-file path) (message "Opened %s — search online (SPC B s) or add manual entry (SPC B m)" path))) ;; Helper: import from DOI → appends to currently open .bib or global (defun my/bib-import-doi () "Fetch BibTeX for a DOI and insert at point (open a .bib file first)." (interactive) (require 'biblio) (require 'biblio-doi) (let ((doi (read-string "DOI: "))) (biblio-doi-insert-bibtex doi))) ;; Helper: insert a manual BibTeX entry template (defun my/bib-insert-manual () "Insert a BibTeX entry template at point for manual editing." (interactive) (let ((type (completing-read "Entry type: " '("article" "book" "inproceedings" "misc" "online" "techreport" "thesis" "manual") nil t))) (insert (pcase type ("article" "@article{KEY,\n author = {},\n title = {},\n journal = {},\n year = {},\n volume = {},\n pages = {},\n doi = {},\n}\n") ("book" "@book{KEY,\n author = {},\n title = {},\n publisher = {},\n year = {},\n isbn = {},\n}\n") ("inproceedings" "@inproceedings{KEY,\n author = {},\n title = {},\n booktitle = {},\n year = {},\n pages = {},\n doi = {},\n}\n") ("misc" "@misc{KEY,\n author = {},\n title = {},\n year = {},\n url = {},\n note = {},\n}\n") ("online" "@online{KEY,\n author = {},\n title = {},\n url = {},\n urldate = {},\n year = {},\n}\n") ("techreport" "@techreport{KEY,\n author = {},\n title = {},\n institution = {},\n year = {},\n number = {},\n}\n") ("thesis" "@thesis{KEY,\n author = {},\n title = {},\n school = {},\n year = {},\n type = {phdthesis},\n}\n") ("manual" "@manual{KEY,\n title = {},\n organization = {},\n year = {},\n url = {},\n}\n"))) ;; Position cursor at KEY for immediate editing (search-backward "KEY" nil t) (message "Fill in KEY and fields. KEY = unique citation identifier (e.g. burns2016)."))) ;; biblio-lookup is the universal search command (prompts for backend) ;; crossref-lookup, arxiv-lookup, dblp-lookup are backend-specific (map! :leader (:prefix ("B" . "bibliography") :desc "Browse references" "b" #'citar-open :desc "Insert citation [cite:]" "i" #'citar-insert-citation :desc "Open notes for reference" "n" #'citar-open-notes :desc "Refresh bibliography" "r" #'citar-refresh :desc "New .bib file" "N" #'my/bib-new :desc "Search online" "s" (cmd! (require 'biblio) (call-interactively #'biblio-lookup)) :desc "Import from DOI" "D" #'my/bib-import-doi :desc "Insert manual entry" "m" #'my/bib-insert-manual :desc "Open global .bib" "f" (cmd! (find-file (expand-file-name "~/org/references.bib"))))) ;;; ============================================================ ;;; ORG-ANKI — flashcards ;;; ============================================================ (use-package! org-anki :commands (org-anki-sync-entry org-anki-sync-all org-anki-delete-entry) :config (setq org-anki-default-deck "Emacs") (map! :map org-mode-map :localleader "A s" #'org-anki-sync-entry "A S" #'org-anki-sync-all "A d" #'org-anki-delete-entry)) ;;; ============================================================ ;;; ORG-QL — query language for org ;;; ============================================================ (use-package! org-ql :commands (org-ql-search org-ql-view org-ql-find) :config (map! :leader "s q" #'org-ql-search "s Q" #'org-ql-view)) ;;; ============================================================ ;;; ORG-ROAM-UI — visual graph ;;; ============================================================ (use-package! org-roam-ui :after org-roam :commands org-roam-ui-mode :config (setq org-roam-ui-sync-theme t org-roam-ui-follow t org-roam-ui-update-on-save t)) ;;; ============================================================ ;;; GRAMMAR CHECK — LanguageTool ;;; ============================================================ (after! langtool (setq langtool-language-tool-jar (expand-file-name "~/languagetool/languagetool-commandline.jar") langtool-default-language "cs" langtool-mother-tongue "cs")) ;;; ============================================================ ;;; KEYBINDINGS — central reference ;;; ============================================================ ;;; Standalone bindings (package-independent, safe to define top-level). ;; ;; The following bindings are defined near their packages (load-order sensitive): ;; SPC o r — elfeed (elfeed section) ;; SPC o n/N — org-noter (org-noter section) ;; SPC o C — calendar/calfw (calfw section) ;; SPC j k/K — link-hint (link-hint section) ;; SPC t o — olivetti (olivetti section) ;; SPC z +/= — zoom in/out (keybindings section below) ;; SPC o M — Matrix/ement (Matrix section) ;; SPC B b/i — bibliography/citar (bibliography section) ;; SPC s q/Q — org-ql (org-ql section) ;; SPC | di| ci| vi| — table cell objects (evil-org section) (map! :leader (:prefix ("z" . "zoom") :desc "Zoom in (x1.5)" "+" #'my/zoom-in :desc "Zoom in (x1.5)" "=" #'my/zoom-in :desc "Zoom out (x1.5)" "-" #'my/zoom-out :desc "Reset" "0" #'my/zoom-reset :desc "Restore previous" "z" #'my/zoom-restore)) (map! :leader :desc "Elfeed" "o r" #'elfeed) (map! :leader (:prefix ("t" . "toggle") :desc "Speech ON" "s" #'my/emacspeak-on :desc "Speech OFF" "S" #'my/emacspeak-off)) (map! :leader (:prefix ("h" . "help") :desc "Describe bindings (buffer-local)" "B" #'describe-bindings)) (map! :leader "o k" #'kubel) (map! :leader "s e" #'iedit-mode) (map! :leader "u" #'vundo) (map! :leader "n r u" #'org-roam-ui-mode) (map! :leader "t g" #'langtool-check "t G" #'langtool-check-done) (map! :leader "o c" #'my/org-caldav-sync) ;; gls (setq insert-directory-program "gls") ;; Always enable macOS accessibility (VoiceOver + Zoom cursor tracking). ;; Override auto-detection so the AX tree is available from startup ;; regardless of whether an AT is currently active. (when (eq system-type 'darwin) (setq ns-accessibility-enabled t))