From 6460823af6610b2d58f965f25f9c35f08f752313 Mon Sep 17 00:00:00 2001 From: Daneel Date: Sat, 21 Feb 2026 00:22:08 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20reorganize=20config.el=20=E2=80=94?= =?UTF-8?q?=20logical=20sections,=20English=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.el | 976 +++++++++++++++++++++++------------------------------- 1 file changed, 416 insertions(+), 560 deletions(-) diff --git a/config.el b/config.el index 3de026c..d8f39fa 100644 --- a/config.el +++ b/config.el @@ -1,90 +1,126 @@ ;;; $DOOMDIR/config.el -*- lexical-binding: t; -*- +;;; ============================================================ +;;; USER IDENTITY +;;; ============================================================ + +(setq user-full-name "Martin Sukany" + user-mail-address "martin@sukany.cz") + + +;;; ============================================================ +;;; THEME & FONT +;;; ============================================================ -;; -------------------------------------------------- -;; Theme / UI -;; -------------------------------------------------- (setq doom-theme 'modus-vivendi-deuteranopia doom-font (font-spec :family "JetBrains Mono" :size 14) doom-variable-pitch-font nil) + (setq display-line-numbers-type t) -;; -------------------------------------------------- -;; macOS / UX -;; -------------------------------------------------- +;;; ============================================================ +;;; 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 — disabled: conflicts with macOS Zoom focus follower +(use-package! centered-cursor-mode + :config + (setq ccm-vpos-init 0.5) + ;; (global-centered-cursor-mode +1) ; uncomment to enable + ) + + +;;; ============================================================ +;;; MACOS / PLATFORM +;;; ============================================================ + (setq mouse-autoselect-window t focus-follows-mouse t select-enable-clipboard t select-enable-primary t inhibit-splash-screen t) -;; ;; -------------------------------------------------- -;; ;; Completion (Company) – minimum, bezpečné -;; ;; -------------------------------------------------- -;; (after! company -;; (setq company-idle-delay 0.1 -;; company-minimum-prefix-length 2 -;; company-selection-wrap-around t -;; company-tooltip-limit 14 -;; company-show-numbers t -;; company-require-match nil) +;; PATH: add MacTeX binaries +(setenv "PATH" (concat "/Library/TeX/texbin:" (getenv "PATH"))) +(add-to-list 'exec-path "/Library/TeX/texbin") -;; ;; Spolehlivé vyvolání v TTY -;; (map! :i "C-." #'company-complete -;; :n "C-." #'company-complete)) +;; macOS clipboard integration via pbcopy/pbpaste (works in terminal Emacs too) +(defun my/pbcopy (text &optional _push) + "Send TEXT to the macOS clipboard using pbcopy." + (let ((process-connection-type nil)) + (let ((proc (start-process "pbcopy" "*pbcopy*" "pbcopy"))) + (process-send-string proc text) + (process-send-eof proc)))) -;; ;; -------------------------------------------------- -;; ;; Robustní file completion "kdekoliv pod kurzorem" -;; ;; -------------------------------------------------- -;; (defun martin/complete-file-name-at-point () -;; "Doplň název souboru kolem kurzoru pomocí standardní file completion tabulky. +(defun my/pbpaste () + "Return text from the macOS clipboard using pbpaste." + (when (executable-find "pbpaste") + (string-trim-right (shell-command-to-string "pbpaste")))) -;; Funguje v libovolném textu, včetně Markdown linků (např. [x](./...))." -;; (interactive) -;; (let* ((stop-chars '(?\s ?\t ?\n ?\r ?\" ?\' ?\( ?\) ?\[ ?\] ?\< ?\> ?\{ ?\} ?, ?\; )) -;; (start (save-excursion -;; (while (and (> (point) (point-min)) -;; (let ((c (char-before))) -;; (and c (not (memq c stop-chars))))) -;; (backward-char)) -;; (point))) -;; (end (save-excursion -;; (while (and (< (point) (point-max)) -;; (let ((c (char-after))) -;; (and c (not (memq c stop-chars))))) -;; (forward-char)) -;; (point)))) -;; (when (= start end) -;; (setq start (point) end (point))) -;; (let ((completion-category-defaults nil) -;; (completion-category-overrides '((file (styles basic partial-completion))))) -;; (completion-in-region start end #'completion-file-name-table)))) +(setq interprogram-cut-function #'my/pbcopy + interprogram-paste-function #'my/pbpaste) -;; (map! :i "C-c f" #'martin/complete-file-name-at-point -;; :n "C-c f" #'martin/complete-file-name-at-point) +;; Let Evil use the system clipboard (y/d/c go to system) +(after! evil + (setq evil-want-clipboard t)) -;; (after! markdown-mode -;; (add-hook 'markdown-mode-hook (lambda () (setq-local company-minimum-prefix-length 1))) -;; (add-hook 'gfm-mode-hook (lambda () (setq-local company-minimum-prefix-length 1)))) +;; 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")))) -;; ------------------------------------------------------------ -;; ORG (Doom-friendly, minimal, bez zbytečných menu) -;; ------------------------------------------------------------ +;;; ============================================================ +;;; PERFORMANCE & GC +;;; ============================================================ + +(setq gc-cons-threshold (* 100 1024 1024) ; 100 MB + 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 + +(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) + +;; !!! 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") + + +;;; ============================================================ +;;; ORG MODE — CORE +;;; ============================================================ (after! org - ;; 0) require packages (require 'ox-hugo) - ;; 1) Kde máš org soubory - ;; Uprav si cestu podle sebe. Tohle je typický Doom default. + (setq org-directory "~/org/") (setq org-default-notes-file (expand-file-name "inbox.org" org-directory)) - (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))) + ;; 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)) (setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "WAIT(w@/!)" "|" "DONE(d!)" "CANCELLED(c@)"))) @@ -95,23 +131,28 @@ 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))) (let ((f (expand-file-name "project.org" root))) (when (file-exists-p f) f)))) - ;; Pomocná funkce: vrátí plnou cestu k souboru v org-directory - (defun ms/org-file (name) - "Return absolute path to NAME inside `org-directory`." - (expand-file-name name org-directory)) + ;; Update all dynamic blocks before export + (add-hook 'org-export-before-processing-hook + (lambda (_backend) (org-update-all-dblocks))) - ;; 2) Org-capture templates - ;; Zachovává tvoje i/n/p a jen doplňuje chybějící věci. + ;; Restore window layout after capture quit + (setq org-capture-restore-window-after-quit t)) + + +;;; ============================================================ +;;; ORG MODE — CAPTURE +;;; ============================================================ + +(after! org (setq org-capture-templates - `( - ;; --- Tvoje původní šablony (beze změn) --- - ("i" "Inbox task" entry + `(("i" "Inbox task" entry (file ,(ms/org-file "inbox.org")) "* TODO %?\n%U\n%a\n") @@ -123,147 +164,145 @@ (file ,(ms/org-file "inbox.org")) "* TODO %? :project:\n%U\n%a\n") - ;; --- Doplňky (minimalisticky) --- - - ;; Subtask do právě clockované položky ("s" "Clocked subtask" entry (clock) "* TODO %?\n%U\n%a\n%i" :empty-lines 1) - ;; Journal do journal.org (datetree) + automatické měření času ("j" "Journal" entry (file+olp+datetree ,(ms/org-file "journal.org")) "\n* %<%I:%M %p> - Journal :journal:\n\n%?\n\n" :clock-in :clock-resume :empty-lines 1) - ;; Meeting do journal.org (datetree) + měření času ("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) - ;; Checking Email – do journal datetree ("e" "Checking Email" entry (file+olp+datetree ,(ms/org-file "journal.org")) "* Checking Email :email:\n\n%?" :clock-in :clock-resume :empty-lines 1) - ;; Weight metrika do tabulky v metrics.org pod headline "Weight" ("w" "Weight" table-line (file+headline ,(ms/org-file "metrics.org") "Weight") "| %U | %^{Weight} | %^{Notes} |" - :kill-buffer t) - ))) + :kill-buffer t)))) -;; ------------------------------------------------------------ -;; (Volitelné) Kvalita života – nic navíc, jen užitečné defaulty -;; ------------------------------------------------------------ + +;;; ============================================================ +;;; ORG MODE — AGENDA +;;; ============================================================ (after! org - ;; ať se po capture vrací do stejného okna (někomu pomáhá) - (setq org-capture-restore-window-after-quit t)) - -(after! org - (add-hook 'org-export-before-processing-hook - (lambda (_backend) - (org-update-all-dblocks)))) + (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)))) -;; -------------------------------------------------- -;; Dired -;; -------------------------------------------------- -(after! dired - (put 'dired-find-alternate-file 'disabled nil) - (map! :map dired-mode-map - "RET" #'dired-find-alternate-file - "^" #'dired-up-directory)) +;;; ============================================================ +;;; ORG MODE — LATEX EXPORT +;;; ============================================================ -;; -------------------------------------------------- -;; PlantUML (server) -;; -------------------------------------------------- -(add-to-list 'auto-mode-alist '("\\.puml\\'" . plantuml-mode)) -(add-to-list 'auto-mode-alist '("\\.plantuml\\'" . plantuml-mode)) +;; Count data columns in an Org table line +(defun my/org-count-table-columns (line) + "Count the number of data columns in Org table LINE." + (length (cl-remove-if + (lambda (s) (string-match-p "^-*$" (string-trim s))) + (cdr (butlast (split-string line "|")))))) -(after! plantuml-mode - (setq plantuml-default-exec-mode 'server - plantuml-server-url "https://www.plantuml.com/plantuml" - plantuml-output-type "svg" - plantuml-verbose t)) +;; Generate tabularx column spec: first column left-aligned, rest Y (auto-width) +(defun my/org-table-attr-latex-spec (ncols) + "Return tabularx column spec for NCOLS columns: first l, rest Y." + (concat "l" (make-string (max 0 (1- ncols)) ?Y))) -(defun my/plantuml-encode-hex (text) - "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) - "Odstraní vše před PNG signaturou." - (let ((sig (unibyte-string #x89 ?P ?N ?G ?\r ?\n #x1a ?\n))) - (with-temp-buffer - (set-buffer-multibyte nil) - (insert-file-contents-literally file) +;; Automatically insert #+ATTR_LATEX tabularx before tables on LaTeX export +(defun my/org-auto-tabularx (backend) + "Insert #+ATTR_LATEX tabularx before each table when exporting to LaTeX." + (when (org-export-derived-backend-p backend 'latex) + (save-excursion (goto-char (point-min)) - (unless (looking-at (regexp-quote sig)) - (let ((pos (search-forward sig nil t))) - (unless pos (user-error "PNG signature nenalezena")) - (delete-region (point-min) (- pos (length sig))) - (let ((coding-system-for-write 'binary)) - (write-region (point-min) (point-max) file nil 'silent))))))) + (while (not (eobp)) + (cond + ((looking-at "^|") + (let ((prev-line (save-excursion + (forward-line -1) + (buffer-substring-no-properties + (line-beginning-position) (line-end-position))))) + (when (not (string-match-p "^|" prev-line)) + (when (not (string-match-p "^#\\+ATTR_LATEX" prev-line)) + (let* ((table-line (buffer-substring-no-properties + (line-beginning-position) (line-end-position))) + (ncols (my/org-count-table-columns table-line)) + (spec (my/org-table-attr-latex-spec ncols)) + (attr (format "#+ATTR_LATEX: :environment tabularx :width \\textwidth :align %s\n" + spec))) + (when (> ncols 0) + (insert attr))))) + (forward-line)) + (t + (forward-line))))))) -(defun my/plantuml-render-server (type) - "Render aktuální .puml přes PlantUML server do PNG nebo SVG." - (interactive (list (completing-read "Type: " '("png" "svg") nil t "png"))) - (unless buffer-file-name (user-error "Otevři .puml jako soubor")) - (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 uložen: %s" out) - out)) +(add-hook 'org-export-before-processing-hook #'my/org-auto-tabularx) -(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")))) - -;; -------------------------------------------------- -;; PATH fix for MacTeX -;; -------------------------------------------------- -(setenv "PATH" (concat "/Library/TeX/texbin:" (getenv "PATH"))) -(add-to-list 'exec-path "/Library/TeX/texbin") - -;; -------------------------------------------------- -;; Python -;; -------------------------------------------------- -(setq python-shell-interpreter "python3") -(after! org - (setq org-babel-python-command "python3") - (require 'ob-python)) +;; Optional: enable booktabs style (horizontal rules in tables) +;; (setq org-latex-tables-booktabs t) +;;; ============================================================ +;;; ORG MODE — CUSTOM BEHAVIOR +;;; ============================================================ -;;; GPTel + OpenWebUI (OpenRouter behind it) +;; Org agenda: position cursor at task name (after TODO keyword and priority) +;; Works with n/p (or j/k in evil mode) — skips TODO keyword and [#A] priority. +(defun my/org-agenda-goto-task-name (&rest _) + "Move cursor to the task name on the current org-agenda line. +Skips past the TODO keyword and optional priority indicator [#A]." + (when (get-text-property (line-beginning-position) 'org-hd-marker) + (beginning-of-line) + (let* ((bol (point)) + (eol (line-end-position)) + (todo-end nil) + (pos bol)) + ;; Find end of TODO keyword by face (org-todo or org-agenda-done) + (while (< pos eol) + (let* ((face (get-text-property pos 'face)) + (next (or (next-single-property-change pos 'face nil eol) eol))) + (when (and face + (or (and (symbolp face) + (memq face '(org-todo org-agenda-done))) + (and (listp face) + (cl-intersection face '(org-todo org-agenda-done))))) + (setq todo-end next)) + (setq pos next))) + ;; Move past TODO keyword and optional priority [#X] + (when todo-end + (goto-char todo-end) + (skip-chars-forward " \t") + (when (looking-at "\\[#.\\][ \t]+") + (goto-char (match-end 0))))))) + +(advice-add 'org-agenda-next-line :after #'my/org-agenda-goto-task-name) +(advice-add 'org-agenda-previous-line :after #'my/org-agenda-goto-task-name) + + +;;; ============================================================ +;;; GPTEL — AI INTEGRATION (OpenWebUI / OpenRouter) +;;; ============================================================ (use-package! gptel :config - ;; 1) API key z env (bez klíčů v configu) + ;; 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"))) - ;; 2) Stáhnout seznam modelů z OpenWebUI /api/models + ;; Fetch available models from OpenWebUI /api/models (defun my/openwebui-fetch-model-ids () - "Return list of model ids from OpenWebUI /api/models (field data[].id)." + "Return list of model ids from OpenWebUI /api/models." (require 'url) (require 'json) (let* ((url-request-method "GET") @@ -272,36 +311,35 @@ (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) ;; přeskočit HTTP hlavičky + (re-search-forward "\n\n" nil 'move) (let* ((json-object-type 'alist) - (json-array-type 'list) - (json-key-type 'symbol) - (obj (json-read)) + (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)))) + (ids (delq nil (mapcar (lambda (it) (alist-get 'id it)) data)))) (kill-buffer (current-buffer)) ids)))) (defvar my/openwebui-models-cache nil) (defun my/openwebui-models () - "Cached list of model ids. Falls back to a minimal list if API fails." + "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) - ;; fallback – zkus běžné OpenRouter ids (může, ale nemusí být dostupné) '("openai/gpt-4o-mini" "openai/gpt-4.1-mini")))))) (defun my/openwebui-refresh-models () - "Clear cache and refetch OpenWebUI model list." + "Clear model cache and refetch from OpenWebUI." (interactive) (setq my/openwebui-models-cache nil) (message "OpenWebUI models refreshed: %d" (length (my/openwebui-models)))) - ;; 3) Backend pro OpenWebUI (OpenAI-compatible chat/completions) + ;; Register OpenWebUI as an OpenAI-compatible backend (setq gptel-backend (gptel-make-openai "OpenWebUI" :host "ai.apps.sukany.cz" @@ -309,51 +347,37 @@ :key #'my/openwebui-key :endpoint "/api/chat/completions" :stream t - ;; často stabilnější přes reverzní proxy/ingress: :curl-args '("--http1.1") :models (my/openwebui-models))) - ;; 4) Default model: ekonomický a praktický - ;; Preferuj openai/gpt-5.2-mini (levný, rychlý), jinak první dostupný model. - ;; (GPT-5.2 mini je běžně uváděn jako “fast, affordable” a na OpenRouter má id openai/gpt-4o-mini) :contentReference[oaicite:2]{index=2} + ;; Default model: prefer gpt-5-mini, fall back to first available (let* ((models (my/openwebui-models)) (preferred "openai/gpt-5-mini")) - (setq gptel-model (if (member preferred models) - preferred - (car models)))) + (setq gptel-model (if (member preferred models) preferred (car models)))) - - ;; 5) Presety (rychlé přepínání podle úlohy) :contentReference[oaicite:3]{index=3} - ;; Pozn.: Presety jen nastavují model/backend/system atd. – žádná magie navíc. + ;; Presets for quick task-specific model switching (gptel-make-preset 'fast - :description "Default (rychlý/levný) – běžná práce" + :description "Default (fast/cheap) — everyday work" :backend "OpenWebUI" :model "openai/gpt-4o-mini" - :system "Odpovídej česky. Buď konkrétní, krokový. Neomáčej to." + :system "Reply in Czech. Be specific and step-by-step. No fluff." :temperature 0.2) (gptel-make-preset 'coding - :description "Kód / refaktor / review (silnější model, když je potřeba)" + :description "Code / refactor / review" :backend "OpenWebUI" - ;; nastav si sem model, který opravdu máš v /api/models: :model "openai/gpt-4.1-mini" - :system "Jsi přísný code reviewer. Navrhuj konkrétní změny a rizika." + :system "You are a strict code reviewer. Propose concrete changes and flag risks." :temperature 0.1) (gptel-make-preset 'deep - :description "Náročná analýza / architektura" + :description "Complex analysis / architecture" :backend "OpenWebUI" - ;; nastav si sem něco silnějšího z tvého seznamu: :model "openai/gpt-4.1" - :system "Postupuj systematicky. Dej varianty, tradeoffs a doporučení." - :temperature 0.2) + :system "Work systematically. Provide alternatives, tradeoffs, and a recommendation." + :temperature 0.2)) - ;; 6) (Volitelné) Debug log, když něco zlobí: - ;; (setq gptel-log-level 'debug) - ) - -;; 7) Mini CLI: volání z command line přes `emacs --batch` -;; Použití níže v příkladech +;; 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) @@ -365,186 +389,225 @@ :callback (lambda (response _info) (setq result response) (setq done t))) - (while (not done) - (accept-process-output nil 0.05)) + (while (not done) (accept-process-output nil 0.05)) (princ result))) - - -;; -------------------------------------------------- -;; GPTel keybindings (safe in Doom) SPC o g ... -;; -------------------------------------------------- +;; GPTel keybindings under SPC o g (after! gptel (map! :leader (:prefix ("o g" . "GPTel") - :desc "GPTel send (region or buffer)" "s" #'gptel-send - :desc "GPTel menu (model/scope/preset)" "m" #'gptel-menu - :desc "GPTel chat buffer" "c" #'gptel - :desc "GPTel abort request" "x" #'gptel-abort - :desc "Refresh OpenWebUI models" "R" #'my/openwebui-refresh-models))) - -;; performance -(setq which-key-idle-delay 0) + :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))) +;;; ============================================================ +;;; COMPLETION — CORFU + CAPE +;;; ============================================================ -;; auto save -(setq auto-save-default nil) ;; zruší #file# bordel -(defun my/save-all-buffers () - (save-some-buffers t)) -(run-with-idle-timer 10 t #'my/save-all-buffers) +(after! corfu + (setq corfu-auto t + corfu-auto-delay 0.15 + corfu-auto-prefix 2 + corfu-cycle t + corfu-preselect 'prompt + corfu-quit-no-match 'separator + corfu-preview-current nil) + (global-corfu-mode)) - -;; centered cursor mode -(use-package! centered-cursor-mode +;; Cape: additional completion-at-point sources +(use-package! cape + :after corfu :config - (setq ccm-vpos-init 0.5) ;; 0.5 = střed okna - ;; VYPNUTO — koliduje s macOS Zoom focus follower (způsobuje skákání obrazu) - ;; (global-centered-cursor-mode +1) - ) + (defun martin/cape-capf-setup () + "Set up cape completion sources for prog-mode and text-mode." + (add-to-list 'completion-at-point-functions #'cape-dabbrev 0) ; words from buffers + (add-to-list 'completion-at-point-functions #'cape-file 0) ; file paths + (add-to-list 'completion-at-point-functions #'cape-keyword 0) ; language keywords + (add-to-list 'completion-at-point-functions #'cape-elisp-symbol 0)) + (add-hook 'prog-mode-hook #'martin/cape-capf-setup) + (add-hook 'text-mode-hook #'martin/cape-capf-setup)) + +;; Corfu popup in terminal (iTerm2 / SSH / tmux) +(use-package! corfu-terminal + :when (not (display-graphic-p)) + :after corfu + :config + (corfu-terminal-mode +1)) +;;; ============================================================ +;;; EMAIL — MU4E +;;; ============================================================ + +(add-to-list 'load-path + (expand-file-name "/opt/homebrew/opt/mu/share/emacs/site-lisp/mu/mu4e")) + +(after! mu4e + (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" + 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 . 22) + (:subject)))) + +(after! mu4e + (setq sendmail-program "msmtp" + message-send-mail-function #'message-send-mail-with-sendmail + mail-specify-envelope-from t + message-sendmail-envelope-from 'header)) -;; Profiling -(setq gc-cons-threshold (* 100 1024 1024) ;; 100 MB - gc-cons-percentage 0.6) +;;; ============================================================ +;;; RSS — ELFEED +;;; ============================================================ -;; GCMH — Doom's GC manager. Zvýšit idle delay (default 15s, ale gcmh ho mění) -;; gcmh-idle-delay ovlivňuje jak často se spouští GC v idle → redisplay → Zoom jump -(after! gcmh - (setq gcmh-idle-delay 'auto ;; nebo konkrétní číslo, např. 60 - gcmh-auto-idle-delay-factor 20 ;; default 10, zvýšeno pro méně GC - gcmh-high-cons-threshold (* 200 1024 1024))) ;; 200MB → GC se spustí méně často +(map! :leader :desc "Elfeed" "o r" #'elfeed) -(add-hook 'focus-out-hook #'garbage-collect) +(after! org + (setq rmh-elfeed-org-files + (list (expand-file-name "elfeed.org" org-directory)))) -(setq doom-modeline-refresh-rate 1.0) - -(setq which-key-idle-delay 0.8 - which-key-idle-secondary-delay 0.05) - -(setq org-idle-time 1.0) - -;; -------------------------------------------------- -;; macOS Zoom accessibility — minimalizace redraws -;; -------------------------------------------------- -;; persp-mode (workspaces) — hlavní viník skákání (2.5s timer) -;; Necháme persp-mode normálně nastartovat, ale po 3s zrušíme jeho cache timer -(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")))) +(after! elfeed + (require 'elfeed-org) + (elfeed-org)) +;;; ============================================================ +;;; PLANTUML +;;; ============================================================ -;; --- macOS clipboard: pbcopy/pbpaste (funguje i v terminal Emacs) --- -(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)))) +(add-to-list 'auto-mode-alist '("\\.puml\\'" . plantuml-mode)) +(add-to-list 'auto-mode-alist '("\\.plantuml\\'" . plantuml-mode)) -(defun my/pbpaste () - "Return text from the macOS clipboard using pbpaste." - (when (executable-find "pbpaste") - (string-trim-right - (shell-command-to-string "pbpaste")))) +(after! plantuml-mode + (setq plantuml-default-exec-mode 'server + plantuml-server-url "https://www.plantuml.com/plantuml" + plantuml-output-type "svg" + plantuml-verbose t)) -(setq select-enable-clipboard t - select-enable-primary 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)))))) -;; Emacs -> system clipboard -(setq interprogram-cut-function #'my/pbcopy) -;; system clipboard -> Emacs -(setq interprogram-paste-function #'my/pbpaste) +(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))))))) -;; Ať Evil používá clipboard (y/d/c budou do systému) -(after! evil - (setq evil-want-clipboard t)) +(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")))) -;; !!! DANGEROUS: disable TLS verification globally !!! -(setq gnutls-verify-error nil) -(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3") +;;; ============================================================ +;;; TRAMP & REMOTE +;;; ============================================================ + +(after! tramp + (setq projectile-git-command "git ls-files -zco --exclude-standard" + projectile-indexing-method 'alien)) + +;; 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)) + +(defadvice projectile-project-root (around ignore-remote first activate) + (unless (file-remote-p default-directory) ad-do-it)) + +(setq remote-file-name-inhibit-cache nil + tramp-verbose 1) + + +;;; ============================================================ +;;; DIRED +;;; ============================================================ + +(after! dired + (put 'dired-find-alternate-file 'disabled nil) + (map! :map dired-mode-map + "RET" #'dired-find-alternate-file + "^" #'dired-up-directory)) + + +;;; ============================================================ +;;; PROJECTILE +;;; ============================================================ (after! projectile - (setq projectile-enable-caching nil - projectile-indexing-method 'alien) + (setq projectile-enable-caching nil + projectile-indexing-method 'alien) (when (executable-find "fd") (setq projectile-generic-command "fd . -0 --type f --hidden --follow --exclude .git --color=never"))) -;; mu4e -(add-to-list 'load-path (expand-file-name "/opt/homebrew/opt/mu/share/emacs/site-lisp/mu/mu4e")) -(after! mu4e - (setq mu4e-maildir "~/.mail" - mu4e-get-mail-command "mbsync personal" - mu4e-update-interval 300 - mu4e-change-filenames-when-moving t - mu4e-view-show-images t)) -(after! mu4e - (setq mu4e-maildir (expand-file-name "~/.mail") - mu4e-get-mail-command "mbsync personal" - mu4e-update-interval 300 - mu4e-change-filenames-when-moving t - mu4e-view-show-images t - ;; TADY je to důležité: - mu4e-sent-folder "/personal/Sent" - mu4e-drafts-folder "/personal/Drafts" - mu4e-trash-folder "/personal/Trash" - mu4e-refile-folder "/personal/Archive")) -(after! mu4e - (setq sendmail-program "msmtp" - message-send-mail-function #'message-send-mail-with-sendmail - mail-specify-envelope-from t - message-sendmail-envelope-from 'header)) +;;; ============================================================ +;;; PYTHON +;;; ============================================================ -(setq user-mail-address "martin@sukany.cz" - user-full-name "Martin Sukany") +(setq python-shell-interpreter "python3") +(after! org + (setq org-babel-python-command "python3") + (require 'ob-python)) -(after! mu4e - ;; thread view - (setq mu4e-headers-show-threads t) - (setq mu4e-headers-include-related t) - ;; pěkné zobrazení - (setq mu4e-headers-fields - '((:human-date . 12) - (:flags . 6) - (:from . 22) - (:subject))) +;;; ============================================================ +;;; ACCESSIBILITY — EMACSPEAK +;;; ============================================================ +;;; Default: OFF. Toggle with SPC t s (on) / SPC t S (off). - ;; strom vláken (lepší orientace) - (setq mu4e-use-fancy-chars t)) -(after! mu4e - (setq mu4e-headers-mark-for-thread t)) - -;;; ================================ -;;; Emacspeak robust ON/OFF for Doom -;;; Default: OFF (won't auto-restart) -;;; Keys: -;;; SPC t s -> Speech ON -;;; SPC t S -> Speech OFF -;;; ================================ - -(defconst my/emacspeak-dir (expand-file-name "~/.emacspeak")) +(defconst my/emacspeak-dir (expand-file-name "~/.emacspeak")) (defconst my/emacspeak-wrapper (expand-file-name "~/.local/bin/emacspeak-mac")) -;; Emacspeak uses this to start the speech server. (setq dtk-program my/emacspeak-wrapper) -;;(setq dtk-program "espeak") -;; State flags -(defvar my/emacspeak-loaded nil) +(defvar my/emacspeak-loaded nil) (defvar my/emacspeak-enabled nil) - -;; Hard inhibit: when non-nil, Emacspeak is NOT allowed to start/restart the server. +;; Hard inhibit: when non-nil, Emacspeak server will not start/restart (defvar my/emacspeak-inhibit-server t) (defun my/emacspeak--ensure-loaded () @@ -552,11 +615,7 @@ (unless my/emacspeak-loaded (setq my/emacspeak-loaded t) (setq emacspeak-directory my/emacspeak-dir) - ;; Load late-ish (but still inside the command that enables it) (load-file (expand-file-name "lisp/emacspeak-setup.el" emacspeak-directory)) - - ;; After Emacspeak is present, install inhibition advices. - ;; These prevent the common 'it restarts by itself' problem. (with-eval-after-load 'dtk-speak (dolist (fn '(dtk-initialize dtk-start-process dtk-speak)) (when (fboundp fn) @@ -564,264 +623,61 @@ fn :around (lambda (orig &rest args) (if my/emacspeak-inhibit-server - ;; OFF mode: do nothing (and crucially: don't restart speaker) - nil + nil ; OFF: do nothing, don't restart (apply orig args))))))))) (defun my/emacspeak-on () - "Enable speech (and allow server start)." + "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."))) + (when (fboundp 'dtk-restart) (ignore-errors (dtk-restart))) + (when (fboundp 'dtk-speak) (ignore-errors (dtk-speak "Emacspeak on."))) (message "Emacspeak ON")) (defun my/emacspeak-off () - "Disable speech robustly (stop + prevent auto-restart)." + "Disable speech and prevent auto-restart." (interactive) - ;; First: inhibit any future attempts to start/restart. - (setq my/emacspeak-enabled nil) - (setq my/emacspeak-inhibit-server t) - - ;; Stop current speech if any. - (when (fboundp 'dtk-stop) - (ignore-errors (dtk-stop))) - - ;; Kill the server process hard (if it exists). + (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)")) -;; Doom leader keys (map! :leader (:prefix ("t" . "toggle") :desc "Speech ON" "s" #'my/emacspeak-on :desc "Speech OFF" "S" #'my/emacspeak-off)) -;; ---------------------------- -;; Emacspeak defaults (global) -;; ---------------------------- (with-eval-after-load 'dtk-speak - ;; Default punctuation mode: none / some / all - ;; (Emacspeak manual: dtk-set-punctuations supports 'none 'some 'all) -(setq dtk-speech-rate-base 300) + (setq dtk-speech-rate-base 300) (setq-default dtk-punctuation-mode 'none)) (with-eval-after-load 'emacspeak - ;; Typing feedback: - ;; nechceš znaky, chceš slova + řádky - ;; (Emacspeak manual: character/word/line echo) - (setq-default emacspeak-character-echo nil) - (setq-default emacspeak-word-echo t) - (setq-default emacspeak-line-echo t)) + (setq-default emacspeak-character-echo nil + emacspeak-word-echo t + emacspeak-line-echo t)) -(map! :leader - (:prefix ("h" . "help") - :desc "Describe bindings (buffer-local)" "B" #'describe-bindings)) - - - -;; 1) ulož si globální default hodnotu +;; Apply global default speech rate after TTS init/restart (setq dtk-default-speech-rate 400) - -;; 2) aplikuj ji ve chvíli, kdy je TTS už inicializované (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) - ;; PREFIX arg => set GLOBAL default (per Emacspeak manual) (ignore-errors (dtk-set-rate dtk-default-speech-rate t)))) - ;; po každé inicializaci/restartu TTS (advice-add 'dtk-initialize :after #'my/dtk-apply-global-default-rate)) -;; ElFeed (RSS) +;;; ============================================================ +;;; KEYBINDINGS +;;; ============================================================ + (map! :leader - :desc "Elfeed" "o r" #'elfeed) - - -(after! org - ;; pokud máš org soubory jinde než ~/org, nastav org-directory sem: - ;; (setq org-directory "~/org") - - ;; elfeed-org: řekni mu explicitně, kde je elfeed.org - (setq rmh-elfeed-org-files - (list (expand-file-name "elfeed.org" org-directory)))) - -(after! elfeed - ;; elfeed-org musí být inicializovaný, jinak se elfeed.org nemusí načítat - (require 'elfeed-org) - (elfeed-org)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; CORFU CONFIGURATION (modern completion UI) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(after! corfu - ;; automatické completions - (setq corfu-auto t - corfu-auto-delay 0.15 - corfu-auto-prefix 2 - - ;; cyklování kandidátů - corfu-cycle t - - ;; předvybrat první kandidát - corfu-preselect 'prompt - - ;; lepší UX - corfu-quit-no-match 'separator - corfu-preview-current nil) - - ;; zapnout globálně - (global-corfu-mode)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; CAPE — zdroje completion -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; Corfu -(after! corfu - (setq corfu-auto t - corfu-auto-delay 0.15 - corfu-auto-prefix 2 - corfu-cycle t) - (global-corfu-mode)) - -;;; Cape (zdroje completion-at-point) -(use-package! cape - :after corfu - :config - (defun martin/cape-capf-setup () - "Zdroje doplňování použitelné téměř všude (bez LSP)." - ;; Slova z bufferů - (add-to-list 'completion-at-point-functions #'cape-dabbrev 0) - ;; Cesty k souborům - (add-to-list 'completion-at-point-functions #'cape-file 0) - ;; Keywords (užitečné hlavně v prog-mode) - (add-to-list 'completion-at-point-functions #'cape-keyword 0) - ;; Elisp symboly (jen když píšeš elisp, ale nevadí ani jinde) - (add-to-list 'completion-at-point-functions #'cape-elisp-symbol 0)) - (add-hook 'prog-mode-hook #'martin/cape-capf-setup) - (add-hook 'text-mode-hook #'martin/cape-capf-setup)) - -;; Corfu popup i v terminálu (iTerm2 / ssh / tmux) -(use-package! corfu-terminal - :when (not (display-graphic-p)) - :after corfu - :config - (corfu-terminal-mode +1)) - -;; TRAMP -(after! tramp - (setq projectile-git-command "git ls-files -zco --exclude-standard" - projectile-indexing-method 'alien)) -;; Vypnout VC a projectile přes TRAMP — hlavní příčina visení -(setq vc-ignore-dir-regexp - (format "%s\\|%s" vc-ignore-dir-regexp tramp-file-name-regexp)) - -;; Vypnout projectile zcela pro remote -(defadvice projectile-project-root (around ignore-remote first activate) - (unless (file-remote-p default-directory) ad-do-it)) - -;; TRAMP cache — neopakovat drahé remote dotazy -(setq remote-file-name-inhibit-cache nil - tramp-verbose 1) - - -;; Latex org export - hack pro spravne tabulky -(defun my/org-count-table-columns (line) - "Spočítej počet datových sloupců v Org table LINE." - (length (cl-remove-if - (lambda (s) (string-match-p "^-*$" (string-trim s))) - (cdr (butlast (split-string line "|")))))) - -(defun my/org-table-attr-latex-spec (ncols) - "Vygeneruj column spec pro tabularx: první sloupec l, zbytek Y (centered X)." - (concat "l" (make-string (max 0 (1- ncols)) ?Y))) - -(defun my/org-auto-tabularx (backend) - "Automaticky přidej #+ATTR_LATEX tabularx před každou tabulku při LaTeX exportu." - (when (org-export-derived-backend-p backend 'latex) - (save-excursion - (goto-char (point-min)) - (while (not (eobp)) - (cond - ;; Řádek začíná | — může být začátek tabulky - ((looking-at "^|") - (let ((prev-line (save-excursion - (forward-line -1) - (buffer-substring-no-properties - (line-beginning-position) (line-end-position))))) - ;; Je to PRVNÍ řádek tabulky? (předchozí řádek NEzačíná |) - (when (not (string-match-p "^|" prev-line)) - ;; Chybí #+ATTR_LATEX? - (when (not (string-match-p "^#\\+ATTR_LATEX" prev-line)) - (let* ((table-line (buffer-substring-no-properties - (line-beginning-position) (line-end-position))) - (ncols (my/org-count-table-columns table-line)) - (spec (my/org-table-attr-latex-spec ncols)) - (attr (format "#+ATTR_LATEX: :environment tabularx :width \\textwidth :align %s\n" - spec))) - (when (> ncols 0) - (insert attr)))))) - (forward-line)) - (t - (forward-line))))))) - -;; Zaregistruj hook — spustí se před každým exportem -(add-hook 'org-export-before-processing-hook #'my/org-auto-tabularx) - -;; -;; Org Agenda: kurzor na název úkolu (za TODO keyword a prioritou) -;; Po každém n/p (nebo j/k v evil mode) přeskočí TODO keyword a [#A] prioritu. -;; -(defun my/org-agenda-goto-task-name (&rest _) - "Přesune kurzor na název úkolu — za TODO keyword a prioritu [#A]." - (when (get-text-property (line-beginning-position) 'org-hd-marker) - (beginning-of-line) - (let* ((bol (point)) - (eol (line-end-position)) - (todo-end nil) - (pos bol)) - ;; Najdi konec TODO keyword podle face (org-todo nebo org-agenda-done) - (while (< pos eol) - (let* ((face (get-text-property pos 'face)) - (next (or (next-single-property-change pos 'face nil eol) eol))) - (when (and face - (or (and (symbolp face) - (memq face '(org-todo org-agenda-done))) - (and (listp face) - (cl-intersection face '(org-todo org-agenda-done))))) - (setq todo-end next)) - (setq pos next))) - ;; Přesuň se za TODO keyword a volitelnou prioritu [#X] - (when todo-end - (goto-char todo-end) - (skip-chars-forward " \t") - (when (looking-at "\\[#.\\][ \t]+") - (goto-char (match-end 0))))))) - -(advice-add 'org-agenda-next-line :after #'my/org-agenda-goto-task-name) -(advice-add 'org-agenda-previous-line :after #'my/org-agenda-goto-task-name) - -;; Volitelně: zapni booktabs styl (horizontal rules v tabulkách) -;; (setq org-latex-tables-booktabs t) - -;; -;; Jak použít: -;; 1. Zkopíruj tento obsah do ~/.config/doom/config.el -;; 2. Spusť: doom sync (nebo M-x doom/reload) -;; 3. Exportuj dokument: SPC m e l p -;; Tabulky se automaticky obalí do tabularx — nic nemusíš přidávat ručně. -;; -;; Chceš jiný výchozí column spec? Uprav my/org-table-attr-latex-spec. -;; Například pro "všechny sloupce rovnoměrně": (make-string ncols ?Y) + (:prefix ("h" . "help") + :desc "Describe bindings (buffer-local)" "B" #'describe-bindings))