Files
emacs-doom/config.el
Martin Sukany 6c77507b19 update
2026-02-08 12:10:38 +01:00

341 lines
13 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 'tango-dark
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
(centered-cursor-mode +1))