Files
emacs-doom/config.el
Martin Sukany c32cffc759 Added
- support for roam
- support for elfeed
- minor changes
- cleared configural
2026-02-13 13:45:08 +01:00

596 lines
21 KiB
EmacsLisp
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
;; --------------------------------------------------
;; 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
;; --------------------------------------------------
(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)
;; Spolehlivé vyvolání v TTY
(map! :i "C-." #'company-complete
:n "C-." #'company-complete))
;; --------------------------------------------------
;; Robustní file completion "kdekoliv pod kurzorem"
;; --------------------------------------------------
(defun martin/complete-file-name-at-point ()
"Doplň název souboru kolem kurzoru pomocí standardní file completion tabulky.
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))))
(map! :i "C-c f" #'martin/complete-file-name-at-point
:n "C-c f" #'martin/complete-file-name-at-point)
(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))))
;;; ~/.doom.d/config.el --- Org config -*- lexical-binding: t; -*-
;; ------------------------------------------------------------
;; ORG (Doom-friendly, minimal, bez zbytečných menu)
;; ------------------------------------------------------------
(after! org
;; 1) Kde máš org soubory
;; Uprav si cestu podle sebe. Tohle je typický Doom default.
(setq org-directory "~/org/")
;; 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))
;; 2) Org-capture templates
;; Zachovává tvoje i/n/p a jen doplňuje chybějící věci.
(setq org-capture-templates
`(
;; --- Tvoje původní šablony (beze změn) ---
("i" "Inbox task" entry
(file ,(ms/org-file "inbox.org"))
"* TODO %?\n%U\n%a\n")
("n" "Note" entry
(file+headline ,(ms/org-file "inbox.org") "Notes")
"* %?\n%U\n%a\n")
("p" "Project task" entry
(file ,(ms/org-file "inbox.org"))
"* TODO %? :project:\n%U\n%a\n")
;; --- 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)
)))
;; ------------------------------------------------------------
;; (Volitelné) Kvalita života nic navíc, jen užitečné defaulty
;; ------------------------------------------------------------
(after! org
;; ať se po capture vrací do stejného okna (někomu pomáhá)
(setq org-capture-restore-window-after-quit t))
;; --------------------------------------------------
;; Dired
;; --------------------------------------------------
(after! dired
(put 'dired-find-alternate-file 'disabled nil)
(map! :map dired-mode-map
"RET" #'dired-find-alternate-file
"^" #'dired-up-directory))
;; --------------------------------------------------
;; PlantUML (server)
;; --------------------------------------------------
(add-to-list 'auto-mode-alist '("\\.puml\\'" . plantuml-mode))
(add-to-list 'auto-mode-alist '("\\.plantuml\\'" . plantuml-mode))
(after! plantuml-mode
(setq plantuml-default-exec-mode 'server
plantuml-server-url "https://www.plantuml.com/plantuml"
plantuml-output-type "svg"
plantuml-verbose t))
(defun my/plantuml-encode-hex (text)
"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)
(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)))))))
(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))
(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))
;;; GPTel + OpenWebUI (OpenRouter behind it)
(use-package! gptel
:config
;; 1) API key z env (bez klíčů v configu)
(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
(defun my/openwebui-fetch-model-ids ()
"Return list of model ids from OpenWebUI /api/models (field data[].id)."
(require 'url)
(require 'json)
(let* ((url-request-method "GET")
(url-request-extra-headers
`(("Authorization" . ,(concat "Bearer " (funcall #'my/openwebui-key))))))
(with-current-buffer (url-retrieve-synchronously
"https://ai.apps.sukany.cz/api/models" t t 15)
(goto-char (point-min))
(re-search-forward "\n\n" nil 'move) ;; přeskočit HTTP hlavičky
(let* ((json-object-type 'alist)
(json-array-type 'list)
(json-key-type 'symbol)
(obj (json-read))
(data (alist-get 'data obj))
(ids (delq nil (mapcar (lambda (it) (alist-get 'id it)) data))))
(kill-buffer (current-buffer))
ids))))
(defvar my/openwebui-models-cache nil)
(defun my/openwebui-models ()
"Cached list of model ids. Falls back to a minimal list if API fails."
(or my/openwebui-models-cache
(setq my/openwebui-models-cache
(condition-case err
(my/openwebui-fetch-model-ids)
(error
(message "OpenWebUI models fetch failed: %s" err)
;; fallback 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."
(interactive)
(setq my/openwebui-models-cache nil)
(message "OpenWebUI models refreshed: %d" (length (my/openwebui-models))))
;; 3) Backend pro OpenWebUI (OpenAI-compatible chat/completions)
(setq gptel-backend
(gptel-make-openai "OpenWebUI"
:host "ai.apps.sukany.cz"
:protocol "https"
: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-4o-mini (levný, rychlý), jinak první dostupný model.
;; (GPT-4o mini je běžně uváděn jako “fast, affordable” a na OpenRouter má id openai/gpt-4o-mini) :contentReference[oaicite:2]{index=2}
(let* ((models (my/openwebui-models))
(preferred "openai/gpt-4o-mini"))
(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.
(gptel-make-preset 'fast
:description "Default (rychlý/levný) běžná práce"
:backend "OpenWebUI"
:model "openai/gpt-4o-mini"
:system "Odpovídej česky. Buď konkrétní, krokový. Neomáčej to."
:temperature 0.2)
(gptel-make-preset 'coding
:description "Kód / refaktor / review (silnější model, když je potřeba)"
: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."
:temperature 0.1)
(gptel-make-preset 'deep
:description "Náročná analýza / architektura"
: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)
;; 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
(defun my/gptel-cli (prompt &optional model system)
"Send PROMPT via gptel and print response to stdout."
(require 'gptel)
(let* ((done nil)
(result nil)
(gptel-model (or model gptel-model))
(gptel--system-message (or system gptel--system-message)))
(gptel-request prompt
:callback (lambda (response _info)
(setq result response)
(setq done t)))
(while (not done)
(accept-process-output nil 0.05))
(princ result)))
;; --------------------------------------------------
;; GPTel keybindings (safe in Doom) 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)
;; auto save
(setq auto-save-default nil) ;; zruší #file# bordel
(defun my/save-all-buffers ()
(save-some-buffers t))
(run-with-idle-timer 5 t #'my/save-all-buffers)
;; centered cursor mode
(use-package! centered-cursor-mode
:config
(setq ccm-vpos-init 0.5) ;; 0.5 = střed okna
(global-centered-cursor-mode +1))
;; Profiling
(setq gc-cons-threshold (* 100 1024 1024) ;; 100 MB
gc-cons-percentage 0.6)
(add-hook 'focus-out-hook #'garbage-collect)
(setq doom-modeline-refresh-rate 1.0) ;; default je 0.10.2
(setq which-key-idle-delay 0.8
which-key-idle-secondary-delay 0.05)
(setq org-idle-time 1.0)
;; --- 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))))
(defun my/pbpaste ()
"Return text from the macOS clipboard using pbpaste."
(when (executable-find "pbpaste")
(string-trim-right
(shell-command-to-string "pbpaste"))))
(setq select-enable-clipboard t
select-enable-primary t)
;; Emacs -> system clipboard
(setq interprogram-cut-function #'my/pbcopy)
;; system clipboard -> Emacs
(setq interprogram-paste-function #'my/pbpaste)
;; Ať Evil používá clipboard (y/d/c budou do systému)
(after! evil
(setq evil-want-clipboard t))
;; !!! DANGEROUS: disable TLS verification globally !!!
(setq gnutls-verify-error nil)
(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")
(after! projectile
(setq projectile-enable-caching nil
projectile-indexing-method 'alien)
(when (executable-find "fd")
(setq projectile-generic-command
"fd . -0 --type f --hidden --follow --exclude .git --color=never")))
;; mu4e
(add-to-list 'load-path (expand-file-name "/opt/homebrew/opt/mu/share/emacs/site-lisp/mu/mu4e"))
(after! mu4e
(setq mu4e-maildir "~/.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))
(setq user-mail-address "martin@sukany.cz"
user-full-name "Martin Sukany")
;;; ================================
;;; 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-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-enabled nil)
;; Hard inhibit: when non-nil, Emacspeak is NOT allowed to start/restart the server.
(defvar my/emacspeak-inhibit-server t)
(defun my/emacspeak--ensure-loaded ()
"Load Emacspeak once, safely, without breaking Doom startup."
(unless my/emacspeak-loaded
(setq my/emacspeak-loaded t)
(setq emacspeak-directory my/emacspeak-dir)
;; Load 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)
(advice-add
fn :around
(lambda (orig &rest args)
(if my/emacspeak-inhibit-server
;; OFF mode: do nothing (and crucially: don't restart speaker)
nil
(apply orig args)))))))))
(defun my/emacspeak-on ()
"Enable speech (and allow server start)."
(interactive)
(setq my/emacspeak-inhibit-server nil)
(my/emacspeak--ensure-loaded)
(setq my/emacspeak-enabled t)
(when (fboundp 'dtk-restart)
(ignore-errors (dtk-restart)))
(when (fboundp 'dtk-speak)
(ignore-errors (dtk-speak "Emacspeak on.")))
(message "Emacspeak ON"))
(defun my/emacspeak-off ()
"Disable speech robustly (stop + 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).
(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-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))
(map! :leader
(:prefix ("h" . "help")
:desc "Describe bindings (buffer-local)" "B" #'describe-bindings))
;; 1) ulož si globální default hodnotu
(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)
(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))