;;; $DOOMDIR/config.el -*- lexical-binding: t; -*- ;; -------------------------------------------------- ;; Theme / UI ;; -------------------------------------------------- (setq doom-theme 'modus-vivendi-deuteranopia display-line-numbers-type t doom-font (font-spec :family "JetBrains Mono" :size 14) doom-variable-pitch-font nil) ;; -------------------------------------------------- ;; 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)))) ;; -------------------------------------------------- ;; Org ;; -------------------------------------------------- (setq org-directory "~/org/") (after! 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 "notes" org-directory))) (setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "WAIT(w@/!)" "|" "DONE(d!)" "CANCELLED(c@)"))) (setq org-log-done 'time) (setq org-refile-targets '((org-agenda-files :maxlevel . 5)) org-outline-path-complete-in-steps nil org-refile-use-outline-path 'file) (defun my/project-org-file () "Return path to ./project.org in current Projectile project, if it exists." (when-let ((root (projectile-project-root))) (let ((f (expand-file-name "project.org" root))) (when (file-exists-p f) f)))) (defun my/org-agenda-project () "Open Org agenda restricted to current project's project.org." (interactive) (let ((org-agenda-files (delq nil (list (my/project-org-file))))) (if org-agenda-files (org-agenda nil "a") (user-error "No project.org found in this project")))) (setq org-capture-templates '(("i" "Inbox task" entry (file "inbox.org") "* TODO %?\n%U\n%a\n") ("n" "Note" entry (file+headline "inbox.org" "Notes") "* %?\n%U\n%a\n") ("p" "Project task" entry (file "inbox.org") "* TODO %? :project:\n%U\n%a\n")))) ;; -------------------------------------------------- ;; 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.1–0.2 (setq which-key-idle-delay 0.8 which-key-idle-secondary-delay 0.05) (setq org-idle-time 1.0) ;; 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") (map! :map mu4e-view-mode-map :localleader "l l" #'org-store-link "l i" #'org-insert-link) (map! :map mu4e-headers-mode-map :localleader "l l" #'org-store-link "l i" #'org-insert-link)