diff --git a/config.el b/config.el index eac38c0..c9b7622 100644 --- a/config.el +++ b/config.el @@ -30,7 +30,6 @@ ;; Start Emacs maximized on every launch (macOS: fills screen, keeps menu bar) (add-to-list 'initial-frame-alist '(fullscreen . maximized)) -;;(add-to-list 'default-frame-alist '(fullscreen . maximized)) ;;; ============================================================ @@ -46,10 +45,9 @@ ;; (NOT "Follow keyboard focus" — that causes viewport jumping) (use-package! centered-cursor-mode :config - (setq ccm-vpos-init 0.5 ; cursor centered on screen - ccm-step-size 2 ; smoother scrolling + (setq ccm-vpos-init 0.5 + ccm-step-size 2 ccm-recenter-at-end-of-file t) - ;; Disable in terminal and special modes (define-globalized-minor-mode my/global-ccm centered-cursor-mode (lambda () @@ -63,26 +61,25 @@ ;;; MACOS / PLATFORM ;;; ============================================================ -(setq mouse-autoselect-window nil ; don't switch window on mouse move - focus-follows-mouse nil ; don't change focus on mouse move +(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, so we add critical -;; directories explicitly. Order matches the shell's $PATH priority. -(let ((extra-paths '("/opt/local/bin" ; MacPorts - "/opt/local/sbin" ; MacPorts sbin - "/opt/homebrew/bin" ; Homebrew (mu, msmtp, …) - "/opt/homebrew/sbin" ; Homebrew sbin - "/usr/local/bin" ; local wrappers (emacs script, …) - "/Library/TeX/texbin"))) ; MacTeX +;; 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 (works in terminal Emacs too) +;; 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)) @@ -98,23 +95,59 @@ (setq interprogram-cut-function #'my/pbcopy interprogram-paste-function #'my/pbpaste) -;; Let Evil use the system clipboard (y/d/c go to system) +;; Let Evil use the system clipboard (after! evil (setq evil-want-clipboard t)) +;; 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)) + +;; Ensure dashboard buffer starts in normal state (required for SPC leader) +(after! evil + (evil-set-initial-state '+doom-dashboard-mode 'normal)) + +;; 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 behaviour: -;; clipboard has image → saves to attachments/ → inserts link -;; clipboard has text → normal paste (yank) -;; -;; Org: [[./attachments/image-TIMESTAMP.png]] -;; Markdown: ![image-TIMESTAMP.png](./attachments/image-TIMESTAMP.png) -;; -;; attachments/ is always relative to the current file's directory. +;; 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)." @@ -124,8 +157,7 @@ (ignore-errors (delete-file tmp)))))) (defun my/paste-image-from-clipboard () - "Save clipboard image to attachments/ subdir and insert link at point. -File: image-YYYYMMDD-HHMMSS.png. Supports org-mode and markdown-mode." + "Save clipboard image to attachments/ and insert link at point." (interactive) (let* ((base-dir (if buffer-file-name (file-name-directory (buffer-file-name)) @@ -141,18 +173,16 @@ File: image-YYYYMMDD-HHMMSS.png. Supports org-mode and markdown-mode." ('org-mode (format "[[./%s]]" relpath)) ('markdown-mode (format "![%s](./%s)" filename relpath)) (_ relpath))) - (message "✓ Image saved: %s" 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 (text paste). -Bound to cmd+v in org-mode and markdown-mode." + "Paste image if clipboard has one, else normal yank." (interactive) (if (my/clipboard-has-image-p) (my/paste-image-from-clipboard) (yank))) -;; Bind cmd+v to smart paste in org and markdown (map! :after org :map org-mode-map "s-v" #'my/smart-paste) @@ -160,75 +190,18 @@ Bound to cmd+v in org-mode and markdown-mode." :map markdown-mode-map "s-v" #'my/smart-paste) -;; macOS Zoom accessibility — cancel persp-mode's 2.5s cache timer after startup -;; (reduces unnecessary redraws that cause 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) - (message "persp-mode 2.5s cache timer cancelled for Zoom accessibility")))) - - -;;; ============================================================ -;;; MACOS GUI — FIXES -;;; ============================================================ - -;; Fix A: Ensure dashboard buffer starts in normal state (required for SPC leader) -(after! evil - (evil-set-initial-state '+doom-dashboard-mode 'normal)) - -;; Fix B: 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)) - -;; Fix C: Disable mouse clicks in GUI Emacs (prevent accidental cursor movement) -;; Scroll wheel events ([wheel-up/down]) are NOT disabled — scrolling works normally. -;; Mouse movement does not switch windows (mouse-autoselect-window nil above). -(when (display-graphic-p) - ;; Disable clicks only — NOT wheel events - (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 best practice (NS/Cocoa build): -;; - pixel-scroll-precision-mode is NOT used: it targets X11/Haiku and breaks -;; NS/Cocoa scroll event delivery (rebinds [wheel-up/down] to non-working handlers) -;; - Standard mwheel.el with conservative settings is reliable on macOS -;; - NS backend converts trackpad + physical wheel to [wheel-up]/[wheel-down] events -(when (display-graphic-p) - (require 'mwheel) - ;; 3 lines per scroll tick; shift = 1 line; no progressive acceleration - (setq mouse-wheel-scroll-amount '(3 ((shift) . 1) ((meta) . 0) ((control) . text-scale)) - mouse-wheel-progressive-speed nil ; constant speed, no acceleration - mouse-wheel-follow-mouse t ; scroll window under cursor - mouse-wheel-tilt-scroll t ; horizontal scroll with tilt/two-finger - mouse-wheel-flip-direction nil) ; standard direction (not natural) - ;; Ensure mwheel-scroll is bound (Doom may remap these) - (global-set-key [wheel-up] #'mwheel-scroll) - (global-set-key [wheel-down] #'mwheel-scroll)) - - ;;; ============================================================ ;;; PERFORMANCE & GC ;;; ============================================================ -(setq gc-cons-threshold (* 100 1024 1024) ; 100 MB +(setq gc-cons-threshold (* 100 1024 1024) gc-cons-percentage 0.6) -;; GCMH — Doom's GC manager; increase idle delay to reduce redraws (after! gcmh (setq gcmh-idle-delay 'auto gcmh-auto-idle-delay-factor 20 - gcmh-high-cons-threshold (* 200 1024 1024))) ; 200 MB + gcmh-high-cons-threshold (* 200 1024 1024))) (add-hook 'focus-out-hook #'garbage-collect) @@ -237,31 +210,29 @@ Bound to cmd+v in org-mode and markdown-mode." (defun my/save-all-buffers () (save-some-buffers t)) (run-with-idle-timer 10 t #'my/save-all-buffers) -;; !!! WARNING: TLS verification disabled globally !!! -;; Required for self-signed certs on local services (ai.apps.sukany.cz etc.) -(setq gnutls-verify-error nil - gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3") +;; TLS: also allow TLS 1.2 fallback for self-signed local services +(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3") ;;; ============================================================ ;;; ORG MODE — CORE ;;; ============================================================ -;; Svátky — pouze české, bez amerických/hebrejských/křesťanských (US) +;; Czech holidays only (no US/Hebrew/Christian defaults) (setq calendar-holidays - '((holiday-fixed 1 1 "Nový rok / Den obnovy samostatnosti ČR") - (holiday-easter-etc -2 "Velký pátek") - (holiday-easter-etc 1 "Velikonoční pondělí") - (holiday-fixed 5 1 "Svátek práce") - (holiday-fixed 5 8 "Den vítězství") - (holiday-fixed 7 5 "Den Cyrila a Metoděje") - (holiday-fixed 7 6 "Den upálení Jana Husa") - (holiday-fixed 9 28 "Den české státnosti") - (holiday-fixed 10 28 "Vznik samostatného Československa") - (holiday-fixed 11 17 "Den boje za svobodu a demokracii") - (holiday-fixed 12 24 "Štědrý den") - (holiday-fixed 12 25 "1. svátek vánoční") - (holiday-fixed 12 26 "2. svátek vánoční"))) + '((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) @@ -269,7 +240,6 @@ Bound to cmd+v in org-mode and markdown-mode." (setq org-directory "~/org/") (setq org-default-notes-file (expand-file-name "inbox.org" org-directory)) - ;; 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)) @@ -283,7 +253,6 @@ Bound to cmd+v in org-mode and markdown-mode." org-outline-path-complete-in-steps nil org-refile-use-outline-path 'file) - ;; Return path to project.org in current Projectile project, if it exists (defun my/project-org-file () "Return path to ./project.org in current Projectile project, if it exists." (when-let ((root (projectile-project-root))) @@ -295,14 +264,13 @@ Bound to cmd+v in org-mode and markdown-mode." (lambda (_backend) (org-update-all-dblocks))) ;; Visual: hide markup, pretty entities, compact tags - (setq org-startup-indented nil ; disable org-indent-mode -- conflicts with org-modern star display + (setq org-startup-indented nil ; conflicts with org-modern star display org-hide-emphasis-markers t org-pretty-entities t org-ellipsis " ▾" org-auto-align-tags nil org-tags-column 0) - ;; Restore window layout after capture quit (setq org-capture-restore-window-after-quit t)) @@ -367,10 +335,9 @@ Bound to cmd+v in org-mode and markdown-mode." ;;; ORG MODE — LATEX EXPORT ;;; ============================================================ -;; LaTeX table export: tabular → tabularx{\linewidth} with lYYY column spec. -;; Uses with-temp-buffer + replace-match (literal t t) — avoids backslash issues -;; in replace-regexp-in-string lambda replacements on Emacs 31. -;; Uses Y column type (defined in document.org template: RaggedRight + auto-width X). +;; 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))))) @@ -378,27 +345,23 @@ Bound to cmd+v in org-mode and markdown-mode." (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. + "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)) - ;; \begin{tabular}{spec} or \begin{tabularx}{spec} → \begin{tabularx}{\linewidth}{lYYY} (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))) - ;; \end{tabular} → \end{tabularx} (skip if already tabularx) (goto-char (point-min)) (while (re-search-forward "\\\\end{tabular}" nil t) (replace-match "\\end{tabularx}" t t)) (buffer-string)))) -;; Register filter on ox-latex load AND ensure it via a pre-processing hook -;; (belt+suspenders: whichever fires first wins, both are idempotent). (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"))) @@ -412,27 +375,19 @@ Skip for beamer exports — beamer uses adjustbox on plain tabular." (add-hook 'org-export-before-processing-hook #'my/org-ensure-tabularx-filter) -;; Optional: enable booktabs style (horizontal rules in tables) -;; (setq org-latex-tables-booktabs t) - -;; Ensure latexmk doesn't hang on errors (nonstopmode + force-continue) -;; Doom latex module usually sets this, but belt+suspenders for macOS. +;; 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 -;; org-latex-remove-logfiles t is the default; extend the list to cover all -;; latexmk output files (bbl, fdb_latexmk, fls, synctex.gz, run.xml, etc.) +;; 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// (created automatically if absent). -;; .tex is intermediate for PDF export -- both routed to exports/pdf/. +;; 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 "\\."))) @@ -452,9 +407,7 @@ Skip for beamer exports — beamer uses adjustbox on plain tabular." ;;; ORG MODE — CUSTOM BEHAVIOR ;;; ============================================================ -;; Org agenda: position cursor at task name (after any todo keyword and priority) -;; Works with n/p (or j/k in evil mode) — skips any keyword from org-todo-keywords -;; (TODO, NEXT, WAIT, DONE, CANCELLED, …) and optional [#A] priority. +;; 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) @@ -464,9 +417,7 @@ Skip for beamer exports — beamer uses adjustbox on plain tabular." (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. -Skips past any org todo keyword (TODO, NEXT, WAIT, DONE, CANCELLED, etc.) -and optional priority indicator [#A]." + "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)) @@ -479,8 +430,7 @@ and optional priority indicator [#A]." (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) -;; Org buffer: snap cursor past TODO keyword/priority on headings in normal state. -;; Fires via post-command-hook (buffer-local) — only when cursor is before task name. +;; 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) @@ -568,7 +518,7 @@ and optional priority indicator [#A]." :curl-args '("--http1.1") :models (my/openwebui-models))) - ;; Default model: prefer gpt-5-mini, fall back to first available + ;; Default model (let* ((models (my/openwebui-models)) (preferred "openai/gpt-5-mini")) (setq gptel-model (if (member preferred models) preferred (car models)))) @@ -620,6 +570,42 @@ and optional priority indicator [#A]." :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 @@ -635,12 +621,11 @@ and optional priority indicator [#A]." corfu-preview-current nil) (global-corfu-mode)) -;; Cape: additional completion-at-point sources (use-package! cape :after corfu :config (defun martin/cape-capf-setup-text () - "Cape sources for text modes (org, markdown, etc.) — no Elisp symbols." + "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 () @@ -653,18 +638,14 @@ and optional priority indicator [#A]." (add-hook 'text-mode-hook #'martin/cape-capf-setup-text) (add-hook 'prog-mode-hook #'martin/cape-capf-setup-prog)) -;; Ensure yasnippet-capf is FIRST in completion-at-point-functions -;; so corfu shows snippets and TAB expands them (not inserts plain text). -;; Depth 0 + local=t puts it before cape backends. (GitHub issue #8183) +;; 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 needed for Emacs < 31 without child-frame support. -;; Emacs 31 handles corfu natively even in terminal; loading corfu-terminal -;; there causes popup positioning issues ("jumping"). +;; 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)) @@ -698,7 +679,6 @@ and optional priority indicator [#A]." (:flags . 6) (:from-or-to . 25) (:subject)) - ;; Sort: newest first mu4e-headers-sort-field :date mu4e-headers-sort-direction 'descending ;; Thread prefixes — fancy Unicode @@ -708,6 +688,7 @@ and optional priority indicator [#A]." mu4e-headers-thread-first-child-prefix '("├>" . "├▶ ") mu4e-headers-thread-last-child-prefix '("└>" . "└▶ ") mu4e-headers-thread-duplicate-prefix '("=" . "≡ ")) + ;; Bookmarks — unread excludes Trash/Archive/Sent/Drafts/Spam (setq mu4e-bookmarks '((:name "Unread" @@ -720,14 +701,11 @@ and optional priority indicator [#A]." ;; Do not cite sender's signature in replies (setq message-cite-function #'message-cite-original-without-signature) - ;; Signature from file — create ~/.mail/signature with your sig text + ;; Signature from file (setq message-signature-file (expand-file-name "~/.mail/signature") message-signature t) ;; Move cursor past headers to message body when opening a message - ;; Modern mu4e (1.8+) uses gnus-article-mode, not mu4e-view-mode. - ;; We use mu4e-view-rendered-hook which fires after the message is displayed, - ;; with a small idle timer to ensure the buffer is fully populated. (defun my/mu4e-view-goto-body () "Position cursor at the start of the message body, skipping headers." (run-with-idle-timer @@ -736,42 +714,36 @@ and optional priority indicator [#A]." (when-let ((buf (get-buffer "*mu4e-article*"))) (with-current-buffer buf (goto-char (point-min)) - ;; Skip header block: lines matching "Key: value" or continuation whitespace (while (and (not (eobp)) (looking-at "^\\([A-Za-z-]+:\\|[ \t]\\)")) (forward-line 1)) - ;; Skip blank separator line(s) (while (and (not (eobp)) (looking-at "^\\s-*$")) (forward-line 1))))))) - ;; Hook into gnus-article-prepare-hook (fires in gnus-based mu4e view) (add-hook 'gnus-article-prepare-hook #'my/mu4e-view-goto-body) - ;; Also keep mu4e-view-mode-hook as fallback for older mu4e / non-gnus view (add-hook 'mu4e-view-mode-hook #'my/mu4e-view-goto-body) + ;; Maildir shortcuts (setq mu4e-maildir-shortcuts '(("/personal/INBOX" . ?i) ("/personal/Sent" . ?s) ("/personal/Trash" . ?t) ("/personal/Archive" . ?a))) + ;; Cursor on subject column after j/k navigation (defun my/mu4e-goto-subject (&rest _) - "Move cursor to the start of the subject text in a mu4e headers line. -Uses the actual subject string from the message rather than column arithmetic, -which is unreliable with unicode flags and thread-prefix strings." + "Move cursor to the start of the subject text in a mu4e 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) - ;; Search for up to the first 10 chars of the subject on this line. - ;; Avoids false matches while tolerating mu4e truncation. (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 on/off (T alone marks thread for bulk action) + ;; zT = toggle thread view (T alone marks thread for bulk action) (evil-define-key 'normal mu4e-headers-mode-map (kbd "zT") #'mu4e-headers-toggle-threading)) @@ -873,15 +845,37 @@ which is unreliable with unicode flags and thread-prefix strings." ;;; ============================================================ -;;; DIRED +;;; DIRED & DIRVISH ;;; ============================================================ +;; 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-mode-line-format '(:left (sort symlink) :right (omit yank index)) + dirvish-attributes '(vc-state subtree-state nerd-icons collapse git-msg file-time file-size) + dirvish-side-width 35) + (map! :map dirvish-mode-map + :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 @@ -892,7 +886,9 @@ which is unreliable with unicode flags and thread-prefix strings." projectile-indexing-method 'alien) (when (executable-find "fd") (setq projectile-generic-command - "fd . -0 --type f --hidden --follow --exclude .git --color=never"))) + "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))) ;;; ============================================================ @@ -904,6 +900,112 @@ which is unreliable with unicode flags and thread-prefix strings." (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 @@ -917,8 +1019,8 @@ which is unreliable with unicode flags and thread-prefix strings." (defvar my/emacspeak-loaded nil) (defvar my/emacspeak-enabled nil) -;; Hard inhibit: when non-nil, Emacspeak server will not start/restart -(defvar my/emacspeak-inhibit-server t) +(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." @@ -933,7 +1035,7 @@ which is unreliable with unicode flags and thread-prefix strings." fn :around (lambda (orig &rest args) (if my/emacspeak-inhibit-server - nil ; OFF: do nothing, don't restart + nil (apply orig args))))))))) (defun my/emacspeak-on () @@ -974,7 +1076,6 @@ which is unreliable with unicode flags and thread-prefix strings." emacspeak-word-echo t emacspeak-line-echo t)) -;; Apply global default speech rate after TTS init/restart (setq dtk-default-speech-rate 400) (with-eval-after-load 'dtk-speak (defun my/dtk-apply-global-default-rate (&rest _) @@ -985,43 +1086,19 @@ which is unreliable with unicode flags and thread-prefix strings." ;;; ============================================================ -;;; ACCESSIBILITY — GLOBAL TEXT SCALING (SPC z) +;;; ACCESSIBILITY — GLOBAL ZOOM (SPC z) ;;; ============================================================ -;; Screen magnifier: scales the global `default' face — all buffers, -;; help windows, doom menus, org-agenda, magit, which-key included. -;; -;; Modeline + header-line + tabs are PINNED to base size (always visible). -;; which-key shows at full zoom in 90 % of frame height — more keys fit. -;; No face-remap hooks (avoid accumulation bugs with timers). -;; -;; Step: ×1.5 per step (multiplicative). From 14pt base: -;; +1 ≈ 21pt +2 ≈ 32pt +3 ≈ 47pt -;; +4 ≈ 71pt +5 ≈ 106pt +6 ≈ 159pt -;; -;; SPC z + / = zoom in -;; SPC z - zoom out -;; SPC z 0 reset to default (saves level for restore) -;; SPC z z restore zoom before last reset -;; In which-key popup: C-h pages to next group of bindings +;; 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. -;; --------------- state --------------- - -(defvar my/zoom-base-height 140 - "Default face height before any zoom, in 1/10 pt. Captured at Doom init.") - -(defvar my/zoom-steps 0 - "Current zoom step count. 0 = default, positive = bigger.") - -(defvar my/zoom-saved-steps nil - "Step count saved before last `my/zoom-reset', for `my/zoom-restore'.") - -;; --------------- pinned faces (always at base) --------------- +(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) - "Faces kept at `my/zoom-base-height' regardless of zoom. -Keeps the status bar and tab bar fully visible at any zoom level.") + header-line tab-bar tab-bar-tab tab-bar-tab-inactive)) (defun my/zoom-pin-ui () "Set all pinned UI faces to base height." @@ -1029,54 +1106,32 @@ Keeps the status bar and tab bar fully visible at any zoom level.") (when (facep face) (set-face-attribute face nil :height my/zoom-base-height)))) -;; --------------- which-key: max side-window height --------------- -;; which-key scales with global zoom (same as all other buffers). -;; Give it 90 % of frame height so more bindings are visible at once. -;; Press C-h while which-key is open to page through remaining bindings. - (after! which-key (setq which-key-side-window-max-height 0.90 which-key-max-display-columns nil)) -;; --------------- core zoom engine --------------- - (defun my/zoom--apply (steps) - "Scale global default face to base × 1.5^STEPS and re-pin UI faces." + "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 ×%.2f ≈%dpt" - steps (expt 1.5 steps) (/ new-h 10)) - ;; Ensure cursor stays visible after zoom change. + (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)) ; reset horizontal scroll + (scroll-right (window-hscroll)) (recenter nil)))) -;; Capture base height once Doom finishes font setup. (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))))) -;; Re-pin UI faces after theme reloads (Doom resets faces on theme change). (add-hook 'doom-load-theme-hook #'my/zoom-pin-ui) -;; --------------- interactive commands --------------- - -(defun my/zoom-in () - "Zoom in one step (×1.5) — all buffers, help, menus, which-key." - (interactive) - (cl-incf my/zoom-steps) - (my/zoom--apply my/zoom-steps)) - -(defun my/zoom-out () - "Zoom out one step (÷1.5) — all buffers." - (interactive) - (cl-decf my/zoom-steps) - (my/zoom--apply my/zoom-steps)) +(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." @@ -1086,7 +1141,7 @@ Keeps the status bar and tab bar fully visible at any zoom level.") (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))) + (message "Zoom reset (SPC z z to restore %+d)" my/zoom-saved-steps))) (defun my/zoom-restore () "Restore zoom level saved before last reset." @@ -1097,7 +1152,6 @@ Keeps the status bar and tab bar fully visible at any zoom level.") (setq my/zoom-steps my/zoom-saved-steps my/zoom-saved-steps nil))) -;; Keep cursor visible while scrolling at any zoom level. (setq hscroll-margin 3 hscroll-step 1 scroll-conservatively 101 @@ -1114,52 +1168,29 @@ Keeps the status bar and tab bar fully visible at any zoom level.") (map! :leader (:prefix ("z" . "zoom") - :desc "Zoom in (×1.5)" "+" #'my/zoom-in - :desc "Zoom in (×1.5)" "=" #'my/zoom-in - :desc "Zoom out (÷1.5)" "-" #'my/zoom-out - :desc "Reset" "0" #'my/zoom-reset - :desc "Restore previous magnification" "z" #'my/zoom-restore)) + :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)) ;;; ============================================================ ;;; MATRIX — EMENT.EL ;;; ============================================================ -;; Matrix client. Package declared in packages.el. -;; Keybindings: SPC o M (open → matrix) -;; Note: SPC o m is taken by Doom's mu4e module (#'mu4e), hence uppercase M. -;; -;; Quick reference: -;; SPC o M o — open Matrix panel (connect + room list, no credentials) -;; SPC o M c — connect / re-connect manually -;; SPC o M C — disconnect -;; SPC o M l — list rooms -;; SPC o M r — open room -;; SPC o M d — direct message +;; Matrix client. SPC o M (open -> matrix). +;; SPC o m is taken by Doom's mu4e module, hence uppercase M. -;; Set BEFORE ement loads — ensures kill-emacs-hook saves sessions on exit. -;; Also pin sessions to a known path (no-littering may redirect otherwise). (setq ement-save-sessions t ement-sessions-file (expand-file-name "ement-sessions.el" doom-private-dir)) (after! ement - - ;; Background auto-sync (internal — do NOT call ement-sync manually, causes issues) - (setq ement-auto-sync t) - - ;; Show timestamp on every message - (setq ement-room-timestamp-format "%H:%M" - ement-room-show-avatars nil) ; avatars slow things down, disabled - - ;; Colored usernames for readability - (setq ement-room-username-display-property '(raise 0)) - - ;; Notify on mentions (@martin) - (setq ement-notify-mentions-p t - ement-notify-dingalings-p nil) ; no sound - - ) ; end after! ement - -;; Defined outside after! so Doom registers them as proper interactive commands. + (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." @@ -1168,51 +1199,38 @@ Keeps the status bar and tab bar fully visible at any zoom level.") (ement-list-rooms))) (defun my/ement-maybe-restore () - "Restore saved ement session silently on startup. No prompts, no buffer. -Loads sessions from file and calls ement--reconnect directly to bypass -any interactive prompts in ement-connect (homeserver discovery, password)." + "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) - ;; Pre-load sessions from file so ement-connect won't prompt (unless ement-sessions (when (fboundp 'ement--load-sessions) (setq ement-sessions (ement--load-sessions)))) - ;; Reconnect directly — bypasses interactive ement-connect entirely (if (fboundp 'ement--reconnect) (dolist (entry ement-sessions) (ement--reconnect (cdr entry))) - ;; Fallback: explicit homeserver avoids discovery/password prompt (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). -If already connected: opens room list immediately. -If sessions file exists: auto-restores without credentials, opens rooms after sync. -Otherwise: runs interactive ement-connect, then opens rooms after sync." + "Open Matrix panel: show room list (connect/restore if needed)." (interactive) (require 'ement) (cond - ;; Already connected — open rooms immediately ((and (boundp 'ement-sessions) ement-sessions) (ement-list-rooms)) - ;; Saved session exists — restore without credentials, open rooms after sync ((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)) - ;; No saved session — interactive connect, open rooms after sync (t (add-hook 'ement-after-initial-sync-hook #'my/ement-open-after-sync) (call-interactively #'ement-connect)))) -;; Auto-connect on Emacs startup (outside after! — ement may be deferred) (add-hook 'doom-after-init-hook #'my/ement-maybe-restore) -;; Keybindings under SPC o M (uppercase M — o m is taken by mu4e) (map! :leader (:prefix ("o M" . "Matrix") :desc "Open panel" "o" #'my/ement-open @@ -1230,28 +1248,18 @@ Otherwise: runs interactive ement-connect, then opens rooms after sync." ;;; PDF TOOLS ;;; ============================================================ -;; pdf-tools: install server binary on first load (after! pdf-tools (pdf-tools-install :no-query)) -;; pdf-view-mode settings (after! pdf-view - ;; Fit page to window width by default (setq-default pdf-view-display-size 'fit-page) - ;; High-res rendering on Retina displays (setq pdf-view-use-scaling t - pdf-view-use-imagemagick nil) - ;; Midnight mode (dark background) — toggle with M-m or keybind - (setq pdf-view-midnight-colors '("#d4d4d4" . "#1c1c1c")) - ;; Continuous scrolling across pages - (setq pdf-view-continuous t) - ;; No line-number gutter - (add-hook 'pdf-view-mode-hook #'(lambda () - (display-line-numbers-mode -1))) - ;; Auto-revert when PDF is regenerated (e.g. after org LaTeX export) + 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)) -;; Evil-friendly keybinds in pdf-view (after! pdf-view (evil-define-key 'normal pdf-view-mode-map "j" #'pdf-view-next-line-or-next-page @@ -1270,10 +1278,7 @@ Otherwise: runs interactive ement-connect, then opens rooms after sync." "a" #'pdf-annot-add-highlight-markup-annotation "q" #'quit-window)) -;; Re-activate Evil normal state when switching to a pdf-view window. -;; Without this, j/k do not respond after SPC w w until the user clicks. -;; Uses targeted advice on window-switch commands to avoid interfering -;; with org-agenda which triggers window-selection-change-functions internally. +;; 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) @@ -1284,39 +1289,28 @@ Otherwise: runs interactive ement-connect, then opens rooms after sync." (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 (pdf-view-mode) instead of Preview +;; 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)))) ; find-file → pdf-view-mode via auto-mode-alist + ("\\.pdf\\'" . emacs)))) ;;; ============================================================ ;;; YASNIPPET — snippets from ~/org/snippets ;;; ============================================================ -;; Load snippets from the emacs-org repo so they are version-controlled. -;; Directory structure: ~/org/snippets// -;; e.g. ~/org/snippets/org-mode/adr, ~/org/snippets/markdown-mode/adr (after! yasnippet - ;; Add ~/org/snippets to yas-snippet-dirs (prepend = higher priority). - ;; Must be in yas-snippet-dirs so it survives yas-reload-all calls. (push (expand-file-name "snippets/" org-directory) yas-snippet-dirs) (yas-reload-all) - ;; TAB in Evil insert: with yasnippet-capf first in CAPF list, - ;; corfu shows snippets as expandable items — no manual TAB override needed. - ;; Standard indent-for-tab-command handles org-cycle / indent fallback. - - ;; Evil + yasnippet: when entering a field, select default text so typing replaces it. - ;; Without this, default text is not visually selected in Evil insert state. + ;; 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)))) - ;; Delete field content with a single key: C-d clears current field content (add-hook 'yas-keymap-disable-hook (lambda () (not (yas--snippets-at-point)))) @@ -1324,8 +1318,8 @@ Otherwise: runs interactive ement-connect, then opens rooms after sync." (lambda () (interactive) (delete-region (yas-field-start (yas-current-field)) - (yas-field-end (yas-current-field))))) - ) + (yas-field-end (yas-current-field)))))) + ;;; ============================================================ ;;; NAVIGATION — link-hint, avy @@ -1347,8 +1341,6 @@ Otherwise: runs interactive ement-connect, then opens rooms after sync." ;;; WRITING — olivetti-mode ;;; ============================================================ -;; Guard: buffer-file-name ensures olivetti doesn't run in export temp buffers. -;; Corfu popup: if it renders off-screen, disable olivetti (SPC t o). (use-package! olivetti :defer t :config @@ -1367,10 +1359,6 @@ Otherwise: runs interactive ement-connect, then opens rooms after sync." ;;; ORG-MODERN ;;; ============================================================ -;;; Org-modern -- modern visual style for org-mode -;; Uses vector star format ["◉"] which works reliably across font/version variations. -;; org-modern-hide-stars: replace non-final stars with · (avoids multi-star clutter). -;; org-modern-table nil: avoids LaTeX export conflicts. (use-package! org-modern :hook (org-mode . org-modern-mode) :hook (org-agenda-finalize . org-modern-agenda) @@ -1378,14 +1366,12 @@ Otherwise: runs interactive ement-connect, then opens rooms after sync." (setq org-modern-star ["◉"] org-modern-hide-stars "·" org-modern-table nil)) -;; org-modern-checkbox: default alist value is correct (☑ ☒ ☐), no override needed ;;; ============================================================ ;;; ORG-FRAGTOG — auto-render LaTeX fragments ;;; ============================================================ -;; Guard: only in file-backed buffers, not in export copies. (use-package! org-fragtog :after org :hook (org-mode . my/org-fragtog-maybe)) @@ -1397,31 +1383,29 @@ Otherwise: runs interactive ement-connect, then opens rooms after sync." ;;; ============================================================ -;;; ORG-SUPER-AGENDA — groups in agenda view +;;; ORG-SUPER-AGENDA ;;; ============================================================ -;; Note: :deadline (before DATE) requires absolute date or eval — unreliable in -;; plain quoted list. "Soon" group removed. Mode enabled via after! (safer). (use-package! org-super-agenda :after org-agenda :config (setq org-super-agenda-groups - '((:name "Kyndryl — dnes" + '((:name "Kyndryl — today" :and (:tag ("kyndryl" "work") :scheduled today)) (:name "Kyndryl — deadline" :and (:tag ("kyndryl" "work") :deadline t)) (:name "Kyndryl" :tag ("kyndryl" "work")) - (:name "ZTJ — dnes" + (:name "ZTJ — today" :and (:tag "ztj" :scheduled today)) (:name "ZTJ" :tag "ztj") - (:name "Dnes" + (:name "Today" :scheduled today :deadline today) - (:name "Cekam" + (:name "Waiting" :todo "WAIT") - (:name "Ostatni" + (:name "Other" :anything t)))) (after! org-super-agenda @@ -1432,37 +1416,20 @@ Otherwise: runs interactive ement-connect, then opens rooms after sync." ;;; ORG-NOTER — PDF annotations ;;; ============================================================ -;; Emacs 31 may not autoload dired-read-dir-and-switches early enough, -;; causing "Symbol's function definition is void" when org-noter starts. -(require 'dired) -;; macOS: use GNU ls (coreutils) for dired/dirvish sorting support -(setq insert-directory-program "gls") - (use-package! org-noter :after (:any org pdf-view) :config (setq org-noter-notes-window-location 'horizontal-split - ;; Directories to search for and create notes files org-noter-notes-search-path (list (expand-file-name "annotations/" org-directory)) - ;; Default note file name candidates org-noter-default-notes-file-names '("notes.org") - ;; Remember last position in PDF across sessions org-noter-auto-save-last-location t - ;; Insert note at precise location, not just page level org-noter-insert-note-no-questions nil org-noter-use-indirect-buffer nil - ;; Split in the current frame (PDF + notes side by side), no new frame org-noter-always-create-frame nil)) -;; Smart org-noter launcher: repairs broken notes files and starts from the PDF window. -;; Starting from the PDF window (not the notes file) ensures org-noter splits the -;; current frame — PDF on one side, notes on the other — instead of creating a -;; hidden new frame. +;; 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. -Repairs any wrong NOTER_DOCUMENT property in the notes file, then -starts the session from the PDF window so the split appears in the -current frame." + "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) @@ -1481,8 +1448,7 @@ current frame." (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 (absolute PDF path). - ;; Absolute paths avoid symlink ambiguity (~/org/ may be an iCloud symlink). + ;; Repair or create notes file with correct NOTER_DOCUMENT path (with-current-buffer (find-file-noselect target) (let ((modified nil)) (save-excursion @@ -1490,7 +1456,6 @@ current frame." (if (re-search-forward (concat "^[ \t]*:" org-noter-property-doc-file ":[ \t]*\\(.*\\)$") nil t) - ;; Property exists — fix if it doesn't point to pdf-path (let* ((stored (string-trim (match-string 1))) (expanded (if (file-name-absolute-p stored) stored @@ -1500,14 +1465,12 @@ current frame." (replace-match (concat ":" org-noter-property-doc-file ": " pdf-path) t t) (setq modified t))) - ;; No property — insert a new org-noter heading (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. cl-letf auto-answers org-noter's prompts for - ;; edge cases where it still asks (e.g. multiple candidate files). + ;; 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) @@ -1525,272 +1488,31 @@ current frame." ;;; ============================================================ -;;; GPTEL — region rewrite & org heading prompt +;;; ORG-CALDAV — CalDAV sync (4 calendars) ;;; ============================================================ - -(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 "Vylepši následující text. Vrať POUZE vylepšený text, nic jiného:\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))) - - -;;; ============================================================ -;;; GIT — git-link -;;; ============================================================ - -(use-package! git-link - :defer t - :config - (setq git-link-default-branch "master") - ;; Add support for Gitea at git.apps.sukany.cz - (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)) - - -;;; ============================================================ -;;; EVIL — ORG TABLE CELL TEXT OBJECTS (di| ci| vi|) -;;; ============================================================ - -;; org-table text objects: "|" = cell (di|, ci|, vi|) -(after! evil-org - ;; Activate all key themes including textobjects - (evil-org-set-key-theme '(navigation insert textobjects additional calendar)) - - ;; Define inner table cell text object - (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)) - - -;;; ============================================================ -;;; FORGE — Gitea integration -;;; ============================================================ -;; Requires Gitea API token in ~/.authinfo: -;; machine git.apps.sukany.cz login daneel^forge password - -(after! forge - (add-to-list 'forge-alist - '("git.apps.sukany.cz" "git.apps.sukany.cz/api/v1" "git.apps.sukany.cz" forge-gitea-repository))) - - -;;; ============================================================ -;;; DEVELOPER WORKFLOW -;;; ============================================================ -;; Language-specific configs for Perl, Python, Go, Ansible, Terraform, Podman. -;; Each language gets SPC m f (format), SPC m r (run), and additional bindings. - -;;; --- Perl (cperl-mode) --- - -;; Prefer cperl-mode over perl-mode for all Perl files -(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)) - -;; Perltidy formatter via reformatter -(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) - - ;; Flycheck: use perl checker - (add-hook 'cperl-mode-hook - (lambda () - (when (fboundp 'flycheck-select-checker) - (flycheck-select-checker 'perl))))) - -;;; --- Python --- - -(after! python - (setq python-shell-interpreter "python3") - - (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)))))) - -;;; --- Go --- - -(after! go-mode - (setq gofmt-command "goimports") - - ;; Auto-format on save - (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)))))) - -;; Ansible-lint via flycheck for YAML files in ansible-ish directories -(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)))))) - -;; Hadolint via flycheck for Dockerfiles -(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))) - -;;; --- Common project settings --- - -(after! projectile - (dolist (dir '("node_modules" "__pycache__" ".terraform" "vendor")) - (add-to-list 'projectile-globally-ignored-directories dir))) - - -;;; ============================================================ -;;; EXTENSIONS — Tier 1-3 + BibTeX -;;; ============================================================ - -;;; --- Tier 1: High impact --- - -;; org-caldav — CalDAV sync (3 calendars) +;; Baikal server: cal.apps.sukany.cz +;; Credentials: ~/.authinfo (machine cal.apps.sukany.cz login martin password ...) ;; -;; ~/.authinfo (chmod 600) musí obsahovat pouze jeden záznam: -;; machine cal.apps.sukany.cz login martin password YOUR_PASSWORD -;; (Rodina sdílena přes Baikal calendarinstances, přístupná pod martin/family/) +;; 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) ;; -;; Soubory: -;; ~/org/calendar_outbox.org — sem piš události které chceš nahrát na server -;; (je v org-agenda-files → zobrazí se v agendě) -;; ~/org/caldav/suky.org — stažené události ze Suky kalendáře (MIMO agendu) -;; ~/org/caldav/placeholders.org — události z Placeholders (MIMO agendu) -;; ~/org/caldav/family.org — rodinný kalendář (MIMO agendu) -;; ~/org/caldav/klara.org — Klářin osobní kalendář (MIMO agendu) -;; -;; Proč caldav/ mimo agendu: stahuje se celá historie ze serveru (i minulé události) -;; a ty by znečistily org-agenda. Obsah caldav/ procházej přes SPC o C (calfw). +;; 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 - ;; Stahuj události i z minulosti (default 60 dní nestačí pro historické události) - (setq org-caldav-days-in-past 1825) ; 5 let zpět — stáhni i historické události - ;; DELETE chování: staré špatné syncy zanechaly :ID: properties v personal.org/work.org - ;; org-caldav je najde přes org-id hash table a ptá se na delete jeden po druhém. - ;; 'never = žádné prompty, žádné mazání z org. Trvalý fix = cleanup :ID: níže. - (setq org-caldav-delete-org-entries 'never) - (setq org-caldav-delete-calendar-entries 'never) + (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/") - ;; State files: explicitně do ~/org/ (default = org-directory, ale radši jistota) - ;; Soubory se jmenují .org-caldav-.el — jeden per calendar URL+ID combo. - ;; Smazáním těchto souborů se resetuje sync state (org-caldav začne od nuly). - (setq org-caldav-save-directory "~/org/") - - ;; Broad error handler: catch any error during cal->org event update - ;; so sync state is saved even if individual events fail (nil fields, etc.) + ;; Error handler: catch errors during cal->org event update + ;; so sync state is saved even if individual events fail (defadvice org-caldav-update-events-in-org (around skip-failed-events activate) "Catch errors during cal->org sync; log and return so sync state is saved." (condition-case err @@ -1800,14 +1522,10 @@ current frame." (org-caldav-debug-print 1 (format "update-events-in-org error: %S" err))))) (defun my/org-caldav-sync () - "Sync 3 CalDAV kalendářů: - 1. Osobni-Suky (default): stahuj vše → caldav-suky.org, nahraj jen calendar_outbox.org - 2. Placeholders: read-only → caldav-placeholders.org - 3. Rodina: read-only → family-calendar.org (login: family)" + "Sync 4 CalDAV calendars: Suky (twoway), Placeholders, Family, Klara (read-only)." (interactive) - ;; Vytvoř caldav/ adresář a prázdné inbox soubory pokud neexistují - ;; (org-caldav někdy odmítne vytvořit soubor sám od sebe) + ;; Ensure caldav/ directory and inbox files exist (make-directory "~/org/caldav" t) (dolist (f '("~/org/caldav/suky.org" "~/org/caldav/placeholders.org" @@ -1817,9 +1535,7 @@ current frame." (with-temp-file (expand-file-name f) (insert "#+TITLE: CalDAV sync\n#+STARTUP: overview\n")))) - ;; --- 1. Osobni - Suky --- - ;; Stahuj události ze serveru → ~/org/caldav/suky.org (mimo agendu) - ;; Nahraj zpět POUZE obsah calendar_outbox.org + ;; 1. Suky (twoway): download -> suky.org, upload from calendar_outbox.org (setq org-caldav-url "https://cal.apps.sukany.cz/dav.php/calendars/martin" org-caldav-calendar-id "default" org-caldav-inbox "~/org/caldav/suky.org" @@ -1827,8 +1543,7 @@ current frame." org-caldav-sync-direction 'twoway) (org-caldav-sync) - ;; --- 2. Placeholders --- - ;; Jen stahuj → ~/org/caldav/placeholders.org (mimo agendu) + ;; 2. Placeholders (read-only) (setq org-caldav-url "https://cal.apps.sukany.cz/dav.php/calendars/martin" org-caldav-calendar-id "4C748EE5-ECFF-4D4A-A72E-6DE37BAADEB3" org-caldav-inbox "~/org/caldav/placeholders.org" @@ -1836,7 +1551,7 @@ current frame." org-caldav-sync-direction 'cal->org) (org-caldav-sync) - ;; --- 3. Rodina (read, sdíleno přes Baikal ACL) --- + ;; 3. Family (read-only, shared via Baikal ACL) (setq org-caldav-url "https://cal.apps.sukany.cz/dav.php/calendars/martin" org-caldav-calendar-id "family" org-caldav-inbox "~/org/caldav/family.org" @@ -1844,7 +1559,7 @@ current frame." org-caldav-sync-direction 'cal->org) (org-caldav-sync) - ;; --- 4. Osobni - Klarka (read, sdíleno přes Baikal ACL) --- + ;; 4. Klara (read-only, shared via Baikal ACL) (setq org-caldav-url "https://cal.apps.sukany.cz/dav.php/calendars/martin" org-caldav-calendar-id "klara" org-caldav-inbox "~/org/caldav/klara.org" @@ -1852,115 +1567,20 @@ current frame." org-caldav-sync-direction 'cal->org) (org-caldav-sync) - (message "CalDAV sync hotov: Suky + Placeholders + Rodina + Klára"))) + (message "CalDAV sync done: Suky + Placeholders + Family + Klara"))) (map! :leader "o c" #'my/org-caldav-sync) -;; envrc — direnv integration -(use-package! envrc - :hook (after-init . envrc-global-mode)) -;; embark — custom prompter (already installed by Doom vertico module) -;; Doom defaultně binduje: C-; → embark-act, SPC a → embark-act -;; Přidáváme C-. jako alias (C-; je primární, C-. pro konzistenci s Emacs konvencí) -(after! embark - (setq embark-prompter #'embark-keymap-prompter) - (map! "C-." #'embark-act - (:map minibuffer-local-map "C-." #'embark-act))) +;;; ============================================================ +;;; CALFW — visual calendar +;;; ============================================================ -;; wgrep — already installed by Doom vertico module, just configure -(after! wgrep - (setq wgrep-auto-save-buffer t)) - -;; kubel — Kubernetes management -(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)) -(map! :leader "o k" #'kubel) - -;;; --- Tier 2: Quality of life --- - -;; org-clock — time tracking (built into org, just configure) -(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)) - -;; iedit — edit multiple occurrences -(use-package! iedit - :commands iedit-mode) -(map! :leader "s e" #'iedit-mode) - -;; vundo — visual undo tree -(use-package! vundo - :commands vundo - :config (setq vundo-glyph-alist vundo-unicode-symbols)) -(map! :leader "u" #'vundo) - -;; breadcrumb — context in header-line -(use-package! breadcrumb - :hook ((prog-mode . breadcrumb-local-mode) - (cperl-mode . breadcrumb-local-mode))) - -;;; --- Tier 3: Supplementary --- - -;; org-anki — Anki flashcards from org -(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)) - -;; calfw — visual calendar (haji-ali fork: calfw-org-open-calendar) (use-package! calfw :demand t) (use-package! calfw-org :demand t :config - ;; calfw v normal state → SPC = Doom leader (SPC b d, SPC b b atd. fungují) - ;; V emacs-state SPC zachytí calfw lokální mapa → nelze použít Doom leader - ;; Řešení: normal state + explicitní rebind calfw navigace (overriduje evil defaults) + ;; 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 @@ -1982,18 +1602,14 @@ current frame." "<" #'calfw-navi-prev-view ">" #'calfw-navi-next-view) - ;; org-caldav vytváří -- pro same-day events → org-element type 'active-range - ;; → calfw-org-convert-org-to-calfw dá tyto eventy do PERIODS (ne contents) - ;; → sorter se na periods nevztahuje → špatné pořadí - ;; Fix: wrapper který sortuje periods list přímo před předáním calfw - + ;; Sort periods (same-day events from org-caldav active-range format) (defun my/calfw-event-time-int (evt) - "Vrátí start-time jako HHMM integer, nebo nil." + "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) - "Sortuje periods sublist v RESULT dle start-time." + "Sort periods sublist in RESULT by start-time." (mapcar (lambda (item) (if (and (listp item) (eq 'periods (car item))) (cons 'periods @@ -2008,7 +1624,7 @@ current frame." result)) (defun my/calfw-sorted-file-source (name file color) - "calfw-org-create-file-source s sortovanými periods." + "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) @@ -2017,7 +1633,6 @@ current frame." (my/calfw-sort-periods (funcall (calfw-source-data base) begin end)))))) - ;; Sorter pro contents (agenda events + file-source contents bez time-range) (defun my/calfw-sorter (x y) (let* ((ex (get-text-property 0 'cfw:event x)) (ey (get-text-property 0 'cfw:event y)) @@ -2028,8 +1643,7 @@ current frame." (cond ((and ta tb) (< ta tb)) (ta t) (tb nil) (t (string-lessp x y))))) (defun my/open-calendar () - "Calfw: org-agenda + CalDAV (Suky=modrá, Klára=žlutá, Rodina=zelená). -org-caldav ukládá same-day events jako active-range → periods → wrapper je sortuje." + "Open calfw with org-agenda + CalDAV sources (Suky, Klara, Family)." (interactive) (condition-case err (let* ((cd (expand-file-name "~/org/caldav/")) @@ -2039,48 +1653,155 @@ org-caldav ukládá same-day events jako active-range → periods → wrapper je (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 "Klára" (concat cd "klara.org") "Gold")) + (my/calfw-sorted-file-source "Klara" (concat cd "klara.org") "Gold")) (when (file-exists-p (concat cd "family.org")) - (my/calfw-sorted-file-source "Rodina" (concat cd "family.org") "ForestGreen")))))) + (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)) + (message "calfw multi-source: %s — fallback" (error-message-string err)) (calfw-org-open-calendar)))) (map! :leader "o C" #'my/open-calendar)) -;; org-roam-ui — visual graph for org-roam -(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)) -(map! :leader "n r u" #'org-roam-ui-mode) -;; dirvish — modern dired replacement -(use-package! dirvish - :init (dirvish-override-dired-mode) - :config - (setq dirvish-mode-line-format '(:left (sort symlink) :right (omit yank index)) - dirvish-attributes '(vc-state subtree-state nerd-icons collapse git-msg file-time file-size) - dirvish-side-width 35) - (map! :map dirvish-mode-map - :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)) +;;; ============================================================ +;;; EXTENSIONS — envrc, embark, wgrep, kubel +;;; ============================================================ -;;; --- BibTeX / Citar (installed by Doom biblio module) --- +(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)) +(map! :leader "o k" #'kubel) + + +;;; ============================================================ +;;; 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) +(map! :leader "s e" #'iedit-mode) + +(use-package! vundo + :commands vundo + :config (setq vundo-glyph-alist vundo-unicode-symbols)) +(map! :leader "u" #'vundo) + +(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))) + + +;;; ============================================================ +;;; BIBTEX / CITAR +;;; ============================================================ (after! citar (setq citar-bibliography '("~/org/references.bib") @@ -2092,7 +1813,51 @@ org-caldav ukládá same-day events jako active-range → periods → wrapper je "b n" #'citar-open-notes "b r" #'citar-refresh)) -;;; --- Grammar check (LanguageTool, installed by Doom grammar module) --- + +;;; ============================================================ +;;; 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)) +(map! :leader "n r u" #'org-roam-ui-mode) + + +;;; ============================================================ +;;; GRAMMAR CHECK — LanguageTool +;;; ============================================================ (after! langtool (setq langtool-language-tool-jar