2145 lines
84 KiB
EmacsLisp
2145 lines
84 KiB
EmacsLisp
;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
|
|
|
|
;;; ============================================================
|
|
;;; USER IDENTITY
|
|
;;; ============================================================
|
|
|
|
(setq user-full-name "Martin Sukany"
|
|
user-mail-address "martin@sukany.cz")
|
|
|
|
;; CalDAV calendar IDs (edit here to update sync targets)
|
|
(defconst my/caldav-url "https://cal.apps.sukany.cz/dav.php/calendars/martin")
|
|
(defconst my/caldav-id-suky "default")
|
|
(defconst my/caldav-id-placeholders "4C748EE5-ECFF-4D4A-A72E-6DE37BAADEB3")
|
|
(defconst my/caldav-id-family "family")
|
|
(defconst my/caldav-id-klara "klara")
|
|
|
|
;; Trust all TLS certificates (corporate MITM proxy with intermediate CA)
|
|
(setq gnutls-verify-error nil)
|
|
(setq tls-checktrust nil)
|
|
(setq network-security-level 'low)
|
|
|
|
|
|
;;; ============================================================
|
|
;;; THEME & FONT
|
|
;;; ============================================================
|
|
|
|
(setq doom-theme 'modus-vivendi-deuteranopia
|
|
doom-variable-pitch-font nil)
|
|
|
|
;; Font: JetBrains Mono preferred; fallback to Menlo (always on macOS)
|
|
;; Install: brew install --cask font-jetbrains-mono
|
|
(setq doom-font (if (find-font (font-spec :name "JetBrains Mono"))
|
|
(font-spec :family "JetBrains Mono" :size 14)
|
|
(font-spec :family "Menlo" :size 14)))
|
|
|
|
(setq display-line-numbers-type t)
|
|
|
|
;; Start Emacs maximized on every launch (macOS: fills screen, keeps menu bar)
|
|
(add-to-list 'initial-frame-alist '(fullscreen . maximized))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; 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 — safe with macOS Zoom set to "Follow mouse cursor"
|
|
;; (NOT "Follow keyboard focus" — that causes viewport jumping)
|
|
(use-package! centered-cursor-mode
|
|
:config
|
|
(setq ccm-vpos-init 0.5
|
|
ccm-step-size 2
|
|
ccm-recenter-at-end-of-file t)
|
|
(define-globalized-minor-mode my/global-ccm
|
|
centered-cursor-mode
|
|
(lambda ()
|
|
(unless (memq major-mode '(vterm-mode eshell-mode term-mode
|
|
treemacs-mode pdf-view-mode))
|
|
(centered-cursor-mode 1))))
|
|
(my/global-ccm +1))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; MACOS / PLATFORM
|
|
;;; ============================================================
|
|
|
|
(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.
|
|
(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
|
|
(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 interprogram-cut-function #'my/pbcopy
|
|
interprogram-paste-function #'my/pbpaste)
|
|
|
|
;; Let Evil use the system clipboard
|
|
(after! evil
|
|
(setq evil-want-clipboard t)
|
|
;; Ensure dashboard buffer starts in normal state (required for SPC leader)
|
|
(evil-set-initial-state '+doom-dashboard-mode 'normal))
|
|
|
|
;; 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))
|
|
|
|
;; 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: 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)."
|
|
(and (executable-find "pngpaste")
|
|
(let ((tmp (make-temp-file "emacs-imgcheck" nil ".png")))
|
|
(prog1 (= 0 (call-process "pngpaste" nil nil nil tmp))
|
|
(ignore-errors (delete-file tmp))))))
|
|
|
|
(defun my/paste-image-from-clipboard ()
|
|
"Save clipboard image to attachments/ and insert link at point."
|
|
(interactive)
|
|
(let* ((base-dir (if buffer-file-name
|
|
(file-name-directory (buffer-file-name))
|
|
default-directory))
|
|
(attach-dir (expand-file-name "attachments" base-dir))
|
|
(filename (format-time-string "image-%Y%m%d-%H%M%S.png"))
|
|
(filepath (expand-file-name filename attach-dir))
|
|
(relpath (concat "attachments/" filename)))
|
|
(make-directory attach-dir t)
|
|
(if (= 0 (call-process "pngpaste" nil nil nil filepath))
|
|
(progn
|
|
(insert (pcase major-mode
|
|
('org-mode (format "[[./%s]]" relpath))
|
|
('markdown-mode (format "" filename relpath))
|
|
(_ 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."
|
|
(interactive)
|
|
(if (my/clipboard-has-image-p)
|
|
(my/paste-image-from-clipboard)
|
|
(yank)))
|
|
|
|
(map! :after org
|
|
:map org-mode-map
|
|
"s-v" #'my/smart-paste)
|
|
(map! :after markdown-mode
|
|
:map markdown-mode-map
|
|
"s-v" #'my/smart-paste)
|
|
|
|
|
|
;;; ============================================================
|
|
;;; PERFORMANCE & GC
|
|
;;; ============================================================
|
|
|
|
(setq gc-cons-threshold (* 100 1024 1024)
|
|
gc-cons-percentage 0.6)
|
|
|
|
(after! gcmh
|
|
(setq gcmh-idle-delay 'auto
|
|
gcmh-auto-idle-delay-factor 20
|
|
gcmh-high-cons-threshold (* 200 1024 1024)))
|
|
|
|
(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)
|
|
|
|
;; TLS: also allow TLS 1.2 fallback for self-signed local services
|
|
(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG MODE — CORE
|
|
;;; ============================================================
|
|
|
|
;; Czech holidays only (no US/Hebrew/Christian defaults)
|
|
(setq calendar-holidays
|
|
'((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)
|
|
|
|
(setq org-directory "~/org/")
|
|
(setq org-default-notes-file (expand-file-name "inbox.org" 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@)")))
|
|
|
|
(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))))
|
|
|
|
;; Update all dynamic blocks before export
|
|
(add-hook 'org-export-before-processing-hook
|
|
(lambda (_backend) (org-update-all-dblocks)))
|
|
|
|
;; Visual: hide markup, pretty entities, compact tags
|
|
(setq org-startup-indented nil ; conflicts with org-modern star display
|
|
org-startup-folded 'content ; show all headings, hide body text
|
|
org-hide-emphasis-markers t
|
|
org-pretty-entities t
|
|
org-ellipsis " ▾"
|
|
org-auto-align-tags nil
|
|
org-tags-column 0)
|
|
|
|
(setq org-capture-restore-window-after-quit t))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG MODE — CAPTURE
|
|
;;; ============================================================
|
|
|
|
(after! org
|
|
(setq org-capture-templates
|
|
`(("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")
|
|
|
|
("s" "Clocked subtask" entry (clock)
|
|
"* TODO %?\n%U\n%a\n%i"
|
|
:empty-lines 1)
|
|
|
|
("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)
|
|
|
|
("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)
|
|
|
|
("e" "Checking Email" entry
|
|
(file+olp+datetree ,(ms/org-file "journal.org"))
|
|
"* Checking Email :email:\n\n%?"
|
|
:clock-in :clock-resume
|
|
:empty-lines 1)
|
|
|
|
("w" "Weight" table-line
|
|
(file+headline ,(ms/org-file "metrics.org") "Weight")
|
|
"| %U | %^{Weight} | %^{Notes} |"
|
|
:kill-buffer t)
|
|
|
|
("c" "Calendar event" entry
|
|
(file ,(ms/org-file "calendar_outbox.org"))
|
|
"* %?\n%(my/calfw-capture-timestamp)\n"
|
|
:empty-lines 1))))
|
|
|
|
;; Ensure ox-icalendar exports active timestamps with times correctly
|
|
(after! ox-icalendar
|
|
(setq org-icalendar-with-timestamps 'active
|
|
org-icalendar-timezone "Europe/Prague"))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG MODE — AGENDA
|
|
;;; ============================================================
|
|
|
|
(after! org
|
|
(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))))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG MODE — LATEX EXPORT
|
|
;;; ============================================================
|
|
|
|
;; 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)))))
|
|
(if (= ncols 1) "Y"
|
|
(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.
|
|
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))
|
|
(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)))
|
|
(goto-char (point-min))
|
|
(while (re-search-forward "\\\\end{tabular}" nil t)
|
|
(replace-match "\\end{tabularx}" t t))
|
|
(buffer-string))))
|
|
|
|
(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")))
|
|
|
|
(defun my/org-ensure-tabularx-filter (backend)
|
|
"Force ox-latex load and register tabularx filter before LaTeX export."
|
|
(when (org-export-derived-backend-p backend 'latex)
|
|
(require 'ox-latex)
|
|
(add-to-list 'org-export-filter-table-functions #'my/org-latex-fix-tabularx)
|
|
(add-to-list 'org-latex-packages-alist '("" "tabularx"))))
|
|
|
|
(add-hook 'org-export-before-processing-hook #'my/org-ensure-tabularx-filter)
|
|
|
|
;; 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
|
|
(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/<type>/
|
|
(defun my/org-export-directory (extension)
|
|
"Return ~/exports/<type>/ directory for file EXTENSION, creating it if needed."
|
|
(let* ((ext (downcase (string-trim-left extension "\\.")))
|
|
(subdir (if (string= ext "tex") "pdf" ext))
|
|
(dir (expand-file-name (concat "exports/" subdir "/") "~")))
|
|
(make-directory dir t)
|
|
dir))
|
|
|
|
(defun my/org-export-output-file-name (orig extension &optional subtreep pub-dir)
|
|
"Route org exports to ~/exports/<type>/ unless pub-dir is already set."
|
|
(funcall orig extension subtreep (or pub-dir (my/org-export-directory extension))))
|
|
|
|
(advice-add 'org-export-output-file-name :around #'my/org-export-output-file-name)
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG MODE — CUSTOM BEHAVIOR
|
|
;;; ============================================================
|
|
|
|
;; 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)
|
|
(dolist (seq org-todo-keywords result)
|
|
(dolist (kw (cdr seq))
|
|
(unless (equal kw "|")
|
|
(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."
|
|
(when (get-text-property (line-beginning-position) 'org-hd-marker)
|
|
(beginning-of-line)
|
|
(let* ((eol (line-end-position))
|
|
(kw-re (regexp-opt (my/org-agenda-all-keywords) 'words)))
|
|
(when (re-search-forward kw-re eol t)
|
|
(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)
|
|
|
|
;; Also trigger on post-command-hook in agenda buffers (catches Evil j/k,
|
|
;; super-agenda navigation, and any other motion commands)
|
|
(add-hook 'org-agenda-mode-hook
|
|
(lambda ()
|
|
(add-hook 'post-command-hook
|
|
#'my/org-agenda-goto-task-name nil t)))
|
|
|
|
;; 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)
|
|
(evil-normal-state-p)
|
|
(org-at-heading-p))
|
|
(let* ((kw-re (regexp-opt (my/org-agenda-all-keywords) 'words))
|
|
(task-start
|
|
(save-excursion
|
|
(beginning-of-line)
|
|
(skip-chars-forward "* ")
|
|
(when (looking-at kw-re)
|
|
(goto-char (match-end 0))
|
|
(skip-chars-forward " \t")
|
|
(when (looking-at "\\[#.\\][ \t]+")
|
|
(goto-char (match-end 0)))
|
|
(point)))))
|
|
(when (and task-start (< (point) task-start))
|
|
(goto-char task-start)))))
|
|
|
|
(add-hook 'org-mode-hook
|
|
(lambda ()
|
|
(add-hook 'post-command-hook
|
|
#'my/org-heading-snap-past-keyword
|
|
nil t)))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; GPTEL — AI INTEGRATION (OpenWebUI / OpenRouter)
|
|
;;; ============================================================
|
|
|
|
(use-package! gptel
|
|
:config
|
|
;; 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")))
|
|
|
|
;; Fetch available models from OpenWebUI /api/models
|
|
(defun my/openwebui-fetch-model-ids ()
|
|
"Return list of model ids from OpenWebUI /api/models."
|
|
(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)
|
|
(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 ()
|
|
"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)
|
|
'("openai/gpt-4o-mini" "openai/gpt-4.1-mini"))))))
|
|
|
|
(defun my/openwebui-refresh-models ()
|
|
"Clear model cache and refetch from OpenWebUI."
|
|
(interactive)
|
|
(setq my/openwebui-models-cache nil)
|
|
(message "OpenWebUI models refreshed: %d" (length (my/openwebui-models))))
|
|
|
|
;; Register OpenWebUI as an OpenAI-compatible backend
|
|
(setq gptel-backend
|
|
(gptel-make-openai "OpenWebUI"
|
|
:host "ai.apps.sukany.cz"
|
|
:protocol "https"
|
|
:key #'my/openwebui-key
|
|
:endpoint "/api/chat/completions"
|
|
:stream t
|
|
:curl-args '("--http1.1")
|
|
:models (my/openwebui-models)))
|
|
|
|
;; Default model
|
|
(let* ((models (my/openwebui-models))
|
|
(preferred "openai/gpt-5-mini"))
|
|
(setq gptel-model (if (member preferred models) preferred (car models))))
|
|
|
|
;; Presets for quick task-specific model switching
|
|
(gptel-make-preset 'fast
|
|
:description "Default (fast/cheap) — everyday work"
|
|
:backend "OpenWebUI"
|
|
:model "openai/gpt-4o-mini"
|
|
:system "Reply in Czech. Be specific and step-by-step. No fluff."
|
|
:temperature 0.2)
|
|
|
|
(gptel-make-preset 'coding
|
|
:description "Code / refactor / review"
|
|
:backend "OpenWebUI"
|
|
:model "openai/gpt-4.1-mini"
|
|
:system "You are a strict code reviewer. Propose concrete changes and flag risks."
|
|
:temperature 0.1)
|
|
|
|
(gptel-make-preset 'deep
|
|
:description "Complex analysis / architecture"
|
|
:backend "OpenWebUI"
|
|
:model "openai/gpt-4.1"
|
|
:system "Work systematically. Provide alternatives, tradeoffs, and a recommendation."
|
|
:temperature 0.2))
|
|
|
|
;; 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)
|
|
(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 under SPC o g
|
|
(after! gptel
|
|
(map! :leader
|
|
(:prefix ("o g" . "GPTel")
|
|
: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)))
|
|
|
|
;; 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
|
|
;;; ============================================================
|
|
|
|
(after! corfu
|
|
(setq corfu-auto t
|
|
corfu-auto-delay 2.0
|
|
corfu-auto-prefix 3 ; need 3+ chars before popup
|
|
corfu-cycle t
|
|
corfu-preselect 'prompt
|
|
corfu-quit-no-match 'separator
|
|
corfu-preview-current nil)
|
|
;; Re-set delay after global-corfu-mode to override Doom defaults
|
|
(add-hook 'global-corfu-mode-hook
|
|
(lambda () (setq corfu-auto-delay 2.0)))
|
|
(global-corfu-mode))
|
|
|
|
(use-package! cape
|
|
:after corfu
|
|
:config
|
|
(defun martin/cape-capf-setup-text ()
|
|
"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 ()
|
|
"Cape sources for prog modes — includes Elisp symbols only in Elisp."
|
|
(add-to-list 'completion-at-point-functions #'cape-dabbrev 0)
|
|
(add-to-list 'completion-at-point-functions #'cape-file 0)
|
|
(add-to-list 'completion-at-point-functions #'cape-keyword 0)
|
|
(when (derived-mode-p 'emacs-lisp-mode)
|
|
(add-to-list 'completion-at-point-functions #'cape-elisp-symbol 0)))
|
|
(add-hook 'text-mode-hook #'martin/cape-capf-setup-text)
|
|
(add-hook 'prog-mode-hook #'martin/cape-capf-setup-prog))
|
|
|
|
;; 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 for Emacs < 31 (31+ handles it natively)
|
|
(use-package! corfu-terminal
|
|
:when (and (not (display-graphic-p))
|
|
(< emacs-major-version 31))
|
|
: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
|
|
;; --- Mailbox layout ---
|
|
(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")
|
|
|
|
;; --- Headers view ---
|
|
(setq 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-or-to . 25)
|
|
(:subject))
|
|
mu4e-headers-sort-field :date
|
|
mu4e-headers-sort-direction 'descending
|
|
;; Thread prefixes — fancy Unicode
|
|
mu4e-headers-thread-single-orphan-prefix '("─>" . "─▶ ")
|
|
mu4e-headers-thread-orphan-prefix '("┬>" . "┬▶ ")
|
|
mu4e-headers-thread-connection-prefix '("│" . "│ ")
|
|
mu4e-headers-thread-first-child-prefix '("├>" . "├▶ ")
|
|
mu4e-headers-thread-last-child-prefix '("└>" . "└▶ ")
|
|
mu4e-headers-thread-duplicate-prefix '("=" . "≡ "))
|
|
|
|
;; --- Bookmarks ---
|
|
;; Keys: u=Unread i=Inbox d=Today w=Week
|
|
;; Note: ?d for Today avoids conflict with maildir shortcut ?t=Trash
|
|
(setq mu4e-bookmarks
|
|
'((:name "Unread"
|
|
:query "flag:unread AND NOT maildir:/personal/Trash AND NOT maildir:/personal/Archive AND NOT maildir:/personal/Sent AND NOT maildir:/personal/Drafts AND NOT maildir:/personal/Spam"
|
|
:key ?u)
|
|
(:name "Inbox" :query "maildir:/personal/INBOX" :key ?i)
|
|
(:name "Today" :query "date:today AND NOT maildir:/personal/Trash AND NOT maildir:/personal/Archive AND NOT maildir:/personal/Sent" :key ?d)
|
|
(:name "Week" :query "date:7d..now AND NOT maildir:/personal/Trash AND NOT maildir:/personal/Archive AND NOT maildir:/personal/Sent" :key ?w)))
|
|
|
|
;; --- Maildir shortcuts (jump with 'j') ---
|
|
;; Keys: i=INBOX s=Sent T=Trash a=Archive
|
|
;; ?T (uppercase) for Trash avoids conflict with bookmark ?t (was Today)
|
|
(setq mu4e-maildir-shortcuts
|
|
'(("/personal/INBOX" . ?i)
|
|
("/personal/Sent" . ?s)
|
|
("/personal/Trash" . ?T)
|
|
("/personal/Archive" . ?a)))
|
|
|
|
;; --- Sending ---
|
|
(setq sendmail-program "msmtp"
|
|
message-send-mail-function #'message-send-mail-with-sendmail
|
|
mail-specify-envelope-from t
|
|
message-sendmail-envelope-from 'header)
|
|
|
|
;; --- Compose / reply ---
|
|
;; message-cite-function is a message-mode setting but configured here
|
|
;; because it only matters in the context of mu4e replies.
|
|
(setq message-cite-function #'message-cite-original-without-signature
|
|
message-signature-file (expand-file-name "~/.mail/signature")
|
|
message-signature t)
|
|
|
|
;; --- Citation colors ---
|
|
;; gnus-cite-* : colors in the view (read) buffer
|
|
;; message-cited-text-*: colors in the compose (reply) buffer
|
|
;; Both use the same Dracula palette for visual consistency.
|
|
;; Duplicate face names are intentional — gnus and message-mode
|
|
;; use separate face systems even though they render the same content.
|
|
(setq gnus-cite-face-list
|
|
'(gnus-cite-1 gnus-cite-2 gnus-cite-3 gnus-cite-4))
|
|
(custom-set-faces!
|
|
'(gnus-cite-1 :foreground "#8be9fd" :italic t) ; cyan — level 1
|
|
'(gnus-cite-2 :foreground "#bd93f9" :italic t) ; purple — level 2
|
|
'(gnus-cite-3 :foreground "#6272a4" :italic t) ; blue — level 3
|
|
'(gnus-cite-4 :foreground "#44475a" :italic t) ; grey — level 4+
|
|
'(message-cited-text-1 :foreground "#8be9fd" :italic t)
|
|
'(message-cited-text-2 :foreground "#bd93f9" :italic t)
|
|
'(message-cited-text-3 :foreground "#6272a4" :italic t)
|
|
'(message-cited-text-4 :foreground "#44475a" :italic t))
|
|
|
|
;; --- View: skip to message body ---
|
|
;; gnus-article-prepare-hook fires when the article buffer is ready,
|
|
;; covering both the initial render and navigation between messages.
|
|
;; No need for mu4e-view-mode-hook (that fires earlier, before content).
|
|
(defun my/mu4e-view-goto-body ()
|
|
"Position cursor at message body, skipping RFC 2822 headers."
|
|
(run-with-idle-timer
|
|
0.05 nil
|
|
(lambda ()
|
|
(when-let ((buf (get-buffer "*mu4e-article*")))
|
|
(with-current-buffer buf
|
|
(goto-char (point-min))
|
|
(while (and (not (eobp))
|
|
(looking-at "^\([A-Za-z-]+:\|[ \t]\)"))
|
|
(forward-line 1))
|
|
(while (and (not (eobp)) (looking-at "^\s-*$"))
|
|
(forward-line 1)))))))
|
|
(add-hook 'gnus-article-prepare-hook #'my/mu4e-view-goto-body)
|
|
|
|
;; --- Headers: keep cursor on subject column after j/k ---
|
|
(defun my/mu4e-goto-subject (&rest _)
|
|
"Move cursor to the start of the subject text in a 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)
|
|
(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 (plain T marks the thread for bulk action)
|
|
(evil-define-key 'normal mu4e-headers-mode-map
|
|
(kbd "zT") #'mu4e-headers-toggle-threading))
|
|
|
|
;;; ============================================================
|
|
;;; RSS — ELFEED
|
|
;;; ============================================================
|
|
|
|
|
|
(after! org
|
|
(setq rmh-elfeed-org-files
|
|
(list (expand-file-name "elfeed.org" org-directory))))
|
|
|
|
(after! elfeed
|
|
(require 'elfeed-org)
|
|
(elfeed-org))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; PLANTUML
|
|
;;; ============================================================
|
|
|
|
(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)
|
|
"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))))))
|
|
|
|
(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)))))))
|
|
|
|
(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"))))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; TRAMP & REMOTE
|
|
;;; ============================================================
|
|
|
|
(after! tramp
|
|
(setq projectile-git-command "git ls-files -zco --exclude-standard"))
|
|
|
|
;; 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))
|
|
|
|
(advice-add 'projectile-project-root :around
|
|
(lambda (orig-fn &rest args)
|
|
(unless (file-remote-p default-directory)
|
|
(apply orig-fn args))))
|
|
|
|
(setq remote-file-name-inhibit-cache nil
|
|
tramp-verbose 1)
|
|
|
|
|
|
;;; ============================================================
|
|
;;; DIRED & DIRVISH
|
|
;;; ============================================================
|
|
|
|
;; Always hide file details (permissions, size, date) for VoiceOver.
|
|
;; Toggle visibility with ( in dired/dirvish buffers.
|
|
;; Three layers of insurance: dirvish-hide-details, dired-mode-hook,
|
|
;; and dired-after-readin-hook (catches late buffer setup).
|
|
(add-hook 'dired-mode-hook #'dired-hide-details-mode)
|
|
;; Attach marked files to mail compose buffer via C-c RET C-a
|
|
(add-hook 'dired-mode-hook #'turn-on-gnus-dired-mode)
|
|
(add-hook 'dired-after-readin-hook
|
|
(lambda () (unless dired-hide-details-mode
|
|
(dired-hide-details-mode 1))))
|
|
|
|
;; 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-hide-details t
|
|
dirvish-mode-line-format '(:left (sort symlink) :right (omit yank index))
|
|
dirvish-attributes '(vc-state subtree-state nerd-icons collapse git-msg)
|
|
dirvish-side-width 35)
|
|
(defun my/dirvish-toggle-details ()
|
|
"Toggle file-time and file-size dirvish attributes."
|
|
(interactive)
|
|
(if (memq 'file-size dirvish-attributes)
|
|
(setq-local dirvish-attributes
|
|
(seq-remove (lambda (a) (memq a '(file-time file-size)))
|
|
dirvish-attributes))
|
|
(setq-local dirvish-attributes
|
|
(append dirvish-attributes '(file-time file-size))))
|
|
(revert-buffer))
|
|
(map! :map dirvish-mode-map
|
|
:n "D" #'my/dirvish-toggle-details
|
|
: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
|
|
;;; ============================================================
|
|
|
|
(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"))
|
|
(dolist (dir '("node_modules" "__pycache__" ".terraform" "vendor"))
|
|
(add-to-list 'projectile-globally-ignored-directories dir)))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; PYTHON
|
|
;;; ============================================================
|
|
|
|
(setq python-shell-interpreter "python3")
|
|
(after! org
|
|
(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
|
|
;;; ============================================================
|
|
;;; Default: OFF. Toggle with SPC t s (on) / SPC t S (off).
|
|
|
|
(defconst my/emacspeak-dir (expand-file-name "~/.emacspeak"))
|
|
(defconst my/emacspeak-wrapper (expand-file-name "~/.local/bin/emacspeak-mac"))
|
|
|
|
(setq dtk-program my/emacspeak-wrapper)
|
|
|
|
(defvar my/emacspeak-loaded nil)
|
|
(defvar my/emacspeak-enabled nil)
|
|
(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."
|
|
(unless my/emacspeak-loaded
|
|
(setq my/emacspeak-loaded t)
|
|
(setq emacspeak-directory my/emacspeak-dir)
|
|
(load-file (expand-file-name "lisp/emacspeak-setup.el" emacspeak-directory))
|
|
(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
|
|
nil
|
|
(apply orig args)))))))))
|
|
|
|
(defun my/emacspeak-on ()
|
|
"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.")))
|
|
(message "Emacspeak ON"))
|
|
|
|
(defun my/emacspeak-off ()
|
|
"Disable speech and prevent auto-restart."
|
|
(interactive)
|
|
(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)"))
|
|
|
|
|
|
(with-eval-after-load 'dtk-speak
|
|
(setq dtk-speech-rate-base 300)
|
|
(setq-default dtk-punctuation-mode 'none))
|
|
|
|
(with-eval-after-load 'emacspeak
|
|
(setq-default emacspeak-character-echo nil
|
|
emacspeak-word-echo t
|
|
emacspeak-line-echo t))
|
|
|
|
(setq dtk-default-speech-rate 400)
|
|
(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)
|
|
(ignore-errors (dtk-set-rate dtk-default-speech-rate t))))
|
|
(advice-add 'dtk-initialize :after #'my/dtk-apply-global-default-rate))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ACCESSIBILITY — GLOBAL ZOOM (SPC z)
|
|
;;; ============================================================
|
|
;; 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.
|
|
|
|
(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))
|
|
|
|
(defun my/zoom-pin-ui ()
|
|
"Set all pinned UI faces to base height."
|
|
(dolist (face my/zoom-pinned-faces)
|
|
(when (facep face)
|
|
(set-face-attribute face nil :height my/zoom-base-height))))
|
|
|
|
(after! which-key
|
|
(setq which-key-side-window-max-height 0.90
|
|
which-key-max-display-columns nil))
|
|
|
|
(defun my/zoom--apply (steps)
|
|
"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 x%.2f ~%dpt" steps (expt 1.5 steps) (/ new-h 10))
|
|
(when (and (not (minibufferp)) (window-live-p (selected-window)))
|
|
(scroll-right (window-hscroll))
|
|
(recenter nil))))
|
|
|
|
(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)))))
|
|
|
|
(add-hook 'doom-load-theme-hook #'my/zoom-pin-ui)
|
|
|
|
(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."
|
|
(interactive)
|
|
(if (= my/zoom-steps 0)
|
|
(message "Zoom: already at default")
|
|
(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)))
|
|
|
|
(defun my/zoom-restore ()
|
|
"Restore zoom level saved before last reset."
|
|
(interactive)
|
|
(if (null my/zoom-saved-steps)
|
|
(message "Zoom: nothing to restore")
|
|
(my/zoom--apply my/zoom-saved-steps)
|
|
(setq my/zoom-steps my/zoom-saved-steps
|
|
my/zoom-saved-steps nil)))
|
|
|
|
(setq hscroll-margin 3
|
|
hscroll-step 1
|
|
scroll-conservatively 101
|
|
scroll-margin 2)
|
|
|
|
|
|
;;; ============================================================
|
|
;;; MATRIX — EMENT.EL
|
|
;;; ============================================================
|
|
;; Matrix client. SPC o M (open -> matrix).
|
|
;; SPC o m is taken by Doom's mu4e module, hence uppercase M.
|
|
|
|
(setq ement-save-sessions t
|
|
ement-sessions-file (expand-file-name "ement-sessions.el" doom-private-dir))
|
|
|
|
(after! ement
|
|
(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."
|
|
(remove-hook 'ement-after-initial-sync-hook #'my/ement-open-after-sync)
|
|
(when (fboundp 'ement-list-rooms)
|
|
(ement-list-rooms)))
|
|
|
|
(defun my/ement-maybe-restore ()
|
|
"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)
|
|
(unless ement-sessions
|
|
(when (fboundp 'ement--load-sessions)
|
|
(setq ement-sessions (ement--load-sessions))))
|
|
(if (fboundp 'ement--reconnect)
|
|
(dolist (entry ement-sessions)
|
|
(ement--reconnect (cdr entry)))
|
|
(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)."
|
|
(interactive)
|
|
(require 'ement)
|
|
(cond
|
|
((and (boundp 'ement-sessions) ement-sessions)
|
|
(ement-list-rooms))
|
|
((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))
|
|
(t
|
|
(add-hook 'ement-after-initial-sync-hook #'my/ement-open-after-sync)
|
|
(call-interactively #'ement-connect))))
|
|
|
|
(add-hook 'doom-after-init-hook #'my/ement-maybe-restore)
|
|
|
|
(map! :leader
|
|
(:prefix ("o M" . "Matrix")
|
|
:desc "Open panel" "o" #'my/ement-open
|
|
:desc "Connect" "c" #'ement-connect
|
|
:desc "Disconnect" "C" #'ement-disconnect
|
|
:desc "List rooms" "l" #'ement-list-rooms
|
|
:desc "Open room" "r" #'ement-view-room
|
|
:desc "Direct message" "d" #'ement-send-direct-message
|
|
:desc "Join room" "j" #'ement-join-room
|
|
:desc "Notifications" "n" #'ement-notifications
|
|
:desc "Mentions" "m" #'ement-mentions))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; PDF TOOLS
|
|
;;; ============================================================
|
|
|
|
(after! pdf-tools
|
|
(pdf-tools-install :no-query))
|
|
|
|
(after! pdf-view
|
|
(setq-default pdf-view-display-size 'fit-page)
|
|
(setq pdf-view-use-scaling t
|
|
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))
|
|
|
|
(after! pdf-view
|
|
(evil-define-key 'normal pdf-view-mode-map
|
|
"j" #'pdf-view-next-line-or-next-page
|
|
"k" #'pdf-view-previous-line-or-previous-page
|
|
"J" #'pdf-view-next-page
|
|
"K" #'pdf-view-previous-page
|
|
"gg" #'pdf-view-first-page
|
|
"G" #'pdf-view-last-page
|
|
"+" #'pdf-view-enlarge
|
|
"-" #'pdf-view-shrink
|
|
"=" #'pdf-view-fit-page-to-window
|
|
"W" #'pdf-view-fit-width-to-window
|
|
"/" #'isearch-forward
|
|
"?" #'isearch-backward
|
|
"m" #'pdf-view-midnight-minor-mode
|
|
"a" #'pdf-annot-add-highlight-markup-annotation
|
|
"q" #'quit-window))
|
|
|
|
;; 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)
|
|
(called-interactively-p 'any))
|
|
(evil-normal-state)))
|
|
|
|
(advice-add 'evil-window-next :after #'my/pdf-view-ensure-normal-state)
|
|
(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 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))))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; YASNIPPET — snippets from ~/org/snippets
|
|
;;; ============================================================
|
|
|
|
(after! yasnippet
|
|
(push (expand-file-name "snippets/" org-directory) yas-snippet-dirs)
|
|
(yas-reload-all)
|
|
|
|
;; 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))))
|
|
|
|
(add-hook 'yas-keymap-disable-hook
|
|
(lambda () (not (yas--snippets-at-point))))
|
|
|
|
(define-key yas-keymap (kbd "C-d")
|
|
(lambda ()
|
|
(interactive)
|
|
(delete-region (yas-field-start (yas-current-field))
|
|
(yas-field-end (yas-current-field))))))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; NAVIGATION — link-hint, avy
|
|
;;; ============================================================
|
|
|
|
(use-package! link-hint
|
|
:defer t
|
|
:commands (link-hint-open-link link-hint-copy-link))
|
|
|
|
(map! :leader
|
|
(:prefix ("j" . "jump")
|
|
:desc "Open link (link-hint)" "k" #'link-hint-open-link
|
|
:desc "Copy link URL" "K" #'link-hint-copy-link
|
|
:desc "Avy goto char-2" "j" #'avy-goto-char-2
|
|
:desc "Avy goto line" "l" #'avy-goto-line))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; WRITING — olivetti-mode
|
|
;;; ============================================================
|
|
|
|
(after! olivetti
|
|
(setq olivetti-body-width 90))
|
|
|
|
(add-hook 'org-mode-hook (lambda () (when buffer-file-name (olivetti-mode 1))))
|
|
(add-hook 'markdown-mode-hook (lambda () (when buffer-file-name (olivetti-mode 1))))
|
|
(add-hook 'text-mode-hook (lambda () (when buffer-file-name (olivetti-mode 1))))
|
|
|
|
(map! :leader
|
|
(:prefix ("t" . "toggle")
|
|
:desc "Olivetti mode" "o" #'olivetti-mode))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG-MODERN
|
|
;;; ============================================================
|
|
|
|
(use-package! org-modern
|
|
:hook (org-mode . org-modern-mode)
|
|
:hook (org-agenda-finalize . org-modern-agenda)
|
|
:config
|
|
(setq org-modern-star ["◉"]
|
|
org-modern-hide-stars "·"
|
|
org-modern-table nil))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG-FRAGTOG — auto-render LaTeX fragments
|
|
;;; ============================================================
|
|
|
|
(use-package! org-fragtog
|
|
:after org
|
|
:hook (org-mode . my/org-fragtog-maybe))
|
|
|
|
(defun my/org-fragtog-maybe ()
|
|
"Enable org-fragtog-mode only in file-backed buffers."
|
|
(when buffer-file-name
|
|
(org-fragtog-mode 1)))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG-SUPER-AGENDA
|
|
;;; ============================================================
|
|
|
|
(use-package! org-super-agenda
|
|
:after org-agenda
|
|
:config
|
|
;; Agenda: start from today, no past days, no duplicates
|
|
(setq org-agenda-start-on-weekday nil
|
|
org-agenda-start-day "0d"
|
|
org-agenda-span 7
|
|
org-agenda-skip-scheduled-if-done t
|
|
org-agenda-skip-deadline-if-done t
|
|
org-agenda-skip-scheduled-if-deadline-is-shown t
|
|
org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled)
|
|
|
|
;; Sorting: priority first, then deadline, then scheduled
|
|
(setq org-agenda-sorting-strategy
|
|
'((agenda priority-down deadline-up scheduled-up)
|
|
(todo priority-down deadline-up)
|
|
(tags priority-down deadline-up)))
|
|
|
|
(setq org-super-agenda-groups
|
|
'((:name "Overdue"
|
|
:deadline past)
|
|
(:name "Due today"
|
|
:deadline today)
|
|
(:name "Scheduled today"
|
|
:scheduled today)
|
|
(:name "Due soon"
|
|
:deadline future)
|
|
(:name "Waiting"
|
|
:todo "WAIT")
|
|
(:name "Kyndryl"
|
|
:tag ("kyndryl" "work"))
|
|
(:name "ZTJ"
|
|
:tag "ztj")
|
|
(:name "Other"
|
|
:anything t))))
|
|
|
|
(after! org-super-agenda
|
|
(org-super-agenda-mode 1)
|
|
;; Fix: org-super-agenda applies its own keymap to group headers
|
|
;; via text properties, overriding org-agenda keybindings.
|
|
;; Setting it to nil disables the header keymap entirely so all
|
|
;; standard agenda keys (m, B, t, etc.) work on every line.
|
|
(setq org-super-agenda-header-map nil))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG-NOTER — PDF annotations
|
|
;;; ============================================================
|
|
|
|
(use-package! org-noter
|
|
:after (:any org pdf-view)
|
|
:config
|
|
(setq org-noter-notes-window-location 'horizontal-split
|
|
org-noter-notes-search-path (list (expand-file-name "annotations/" org-directory))
|
|
org-noter-default-notes-file-names '("notes.org")
|
|
org-noter-auto-save-last-location t
|
|
org-noter-insert-note-no-questions nil
|
|
org-noter-use-indirect-buffer nil
|
|
org-noter-always-create-frame nil))
|
|
|
|
;; 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."
|
|
(interactive)
|
|
(require 'org-noter)
|
|
(let* ((pdf-win (if (derived-mode-p 'pdf-view-mode)
|
|
(selected-window)
|
|
(cl-find-if (lambda (w)
|
|
(with-current-buffer (window-buffer w)
|
|
(derived-mode-p 'pdf-view-mode)))
|
|
(window-list (selected-frame)))))
|
|
(pdf-path (when pdf-win
|
|
(with-current-buffer (window-buffer pdf-win)
|
|
(buffer-file-name)))))
|
|
(unless pdf-path
|
|
(user-error "No PDF buffer visible — open a PDF first"))
|
|
(let* ((base (file-name-base pdf-path))
|
|
(notes-name (concat base ".org"))
|
|
(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 path
|
|
(with-current-buffer (find-file-noselect target)
|
|
(let ((modified nil))
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(if (re-search-forward
|
|
(concat "^[ \t]*:" org-noter-property-doc-file ":[ \t]*\\(.*\\)$")
|
|
nil t)
|
|
(let* ((stored (string-trim (match-string 1)))
|
|
(expanded (if (file-name-absolute-p stored)
|
|
stored
|
|
(expand-file-name stored (file-name-directory target)))))
|
|
(unless (and (file-exists-p expanded)
|
|
(file-equal-p expanded pdf-path))
|
|
(replace-match
|
|
(concat ":" org-noter-property-doc-file ": " pdf-path) t t)
|
|
(setq modified t)))
|
|
(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 for proper frame split
|
|
(cl-letf* ((orig-cr (symbol-function 'completing-read))
|
|
((symbol-function 'completing-read)
|
|
(lambda (prompt collection &rest args)
|
|
(cond
|
|
((string-match-p "name" prompt) notes-name)
|
|
((string-match-p "save\\|where\\|Which" prompt) target)
|
|
(t (apply orig-cr prompt collection args))))))
|
|
(with-selected-window pdf-win
|
|
(org-noter))))))
|
|
|
|
(map! :leader
|
|
(:prefix ("o" . "open")
|
|
:desc "org-noter" "n" #'my/org-noter-start
|
|
:desc "org-noter insert note" "N" #'org-noter-insert-note))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG-CALDAV — CalDAV sync (4 calendars)
|
|
;;; ============================================================
|
|
;; Baikal server: cal.apps.sukany.cz
|
|
;; Credentials: ~/.authinfo (machine cal.apps.sukany.cz login martin password ...)
|
|
;;
|
|
;; 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)
|
|
;;
|
|
;; 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
|
|
(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/")
|
|
|
|
;; Error handler: catch errors during cal->org event update
|
|
;; so sync state is saved even if individual events fail
|
|
(advice-add 'org-caldav-update-events-in-org :around
|
|
(lambda (orig-fn &rest args)
|
|
"Catch errors during cal->org sync; log and return so sync state is saved."
|
|
(condition-case err
|
|
(apply orig-fn args)
|
|
(error
|
|
(message "org-caldav: update-events-in-org error (sync continues): %S" err)
|
|
(org-caldav-debug-print 1 (format "update-events-in-org error: %S" err))))))
|
|
|
|
(defun my/org-caldav-sync ()
|
|
"Sync 4 CalDAV calendars: Suky (twoway), Placeholders, Family, Klara (read-only)."
|
|
(interactive)
|
|
|
|
;; Ensure caldav/ directory and inbox files exist
|
|
(make-directory "~/org/caldav" t)
|
|
(dolist (f '("~/org/caldav/suky.org"
|
|
"~/org/caldav/placeholders.org"
|
|
"~/org/caldav/family.org"
|
|
"~/org/caldav/klara.org"))
|
|
(unless (file-exists-p (expand-file-name f))
|
|
(with-temp-file (expand-file-name f)
|
|
(insert "#+TITLE: CalDAV sync\n#+STARTUP: overview\n"))))
|
|
|
|
;; 1. Suky (twoway): download -> suky.org, upload from calendar_outbox.org
|
|
(setq org-caldav-url my/caldav-url
|
|
org-caldav-calendar-id my/caldav-id-suky
|
|
org-caldav-inbox "~/org/caldav/suky.org"
|
|
org-caldav-files '("~/org/calendar_outbox.org")
|
|
org-caldav-sync-direction 'twoway)
|
|
(org-caldav-sync)
|
|
|
|
;; 2. Placeholders (read-only)
|
|
(setq org-caldav-url my/caldav-url
|
|
org-caldav-calendar-id my/caldav-id-placeholders
|
|
org-caldav-inbox "~/org/caldav/placeholders.org"
|
|
org-caldav-files nil
|
|
org-caldav-sync-direction 'cal->org)
|
|
(org-caldav-sync)
|
|
|
|
;; 3. Family (read-only, shared via Baikal ACL)
|
|
(setq org-caldav-url my/caldav-url
|
|
org-caldav-calendar-id my/caldav-id-family
|
|
org-caldav-inbox "~/org/caldav/family.org"
|
|
org-caldav-files nil
|
|
org-caldav-sync-direction 'cal->org)
|
|
(org-caldav-sync)
|
|
|
|
;; 4. Klara (read-only, shared via Baikal ACL)
|
|
(setq org-caldav-url my/caldav-url
|
|
org-caldav-calendar-id my/caldav-id-klara
|
|
org-caldav-inbox "~/org/caldav/klara.org"
|
|
org-caldav-files nil
|
|
org-caldav-sync-direction 'cal->org)
|
|
(org-caldav-sync)
|
|
|
|
(message "CalDAV sync done: Suky + Placeholders + Family + Klara")))
|
|
|
|
|
|
|
|
;;; ============================================================
|
|
;;; CALFW — visual calendar
|
|
;;; ============================================================
|
|
|
|
(use-package! calfw :demand t)
|
|
(use-package! calfw-org
|
|
:demand t
|
|
:config
|
|
;; org-capture integration: "a" to add event on selected date
|
|
;; Store date from calfw before capture starts
|
|
(defvar my/calfw-capture-date nil "Date selected in calfw for capture.")
|
|
|
|
(defun my/calfw-capture ()
|
|
"Start org-capture with calfw date context."
|
|
(interactive)
|
|
(setq my/calfw-capture-date (calfw-cursor-to-nearest-date))
|
|
(org-capture nil "c"))
|
|
|
|
;; Prompts for start/end time; empty = all-day event
|
|
;; Uses active timestamp with time range for timed events,
|
|
;; or date-range (-->--) for multi-day events.
|
|
(defun my/calfw-capture-timestamp ()
|
|
"Build org timestamp for selected calfw date with optional time prompt.
|
|
Formats matching what org-caldav/ox-icalendar export correctly:
|
|
- All-day single: <2026-02-26 Thu>
|
|
- Timed single day: <2026-02-26 Thu 14:00-15:30>
|
|
- All-day multi: <2026-02-26 Thu>--<2026-02-28 Sat>
|
|
- Timed multi-day: <2026-02-26 Thu 15:00>--<2026-02-28 Sat 22:00>"
|
|
(let* ((date (or my/calfw-capture-date (calendar-current-date)))
|
|
(m (nth 0 date))
|
|
(d (nth 1 date))
|
|
(y (nth 2 date))
|
|
(dow (format-time-string "%a" (encode-time 0 0 12 d m y)))
|
|
(start-time (read-string "Start time (HH:MM, empty=all-day): "))
|
|
(end-input (unless (string-empty-p start-time)
|
|
(read-string "End time or end date (HH:MM / YYYY-MM-DD / YYYY-MM-DD HH:MM, empty=same day open): "))))
|
|
(cond
|
|
;; All-day event (no time given)
|
|
((string-empty-p start-time)
|
|
(let ((end-date (read-string "End date for multi-day (YYYY-MM-DD, empty=single day): ")))
|
|
(if (string-empty-p end-date)
|
|
(format "<%04d-%02d-%02d %s>" y m d dow)
|
|
(let* ((parsed (parse-time-string (concat end-date " 00:00")))
|
|
(ed (nth 3 parsed)) (em (nth 4 parsed)) (ey (nth 5 parsed))
|
|
(edow (format-time-string "%a" (encode-time 0 0 12 ed em ey))))
|
|
(format "<%04d-%02d-%02d %s>--<%04d-%02d-%02d %s>" y m d dow ey em ed edow)))))
|
|
;; Timed event - check if end contains a date (multi-day)
|
|
((and end-input (not (string-empty-p end-input))
|
|
(string-match "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}" end-input))
|
|
;; Multi-day with times
|
|
(let* ((end-time-part (if (string-match " \\([0-9]\\{2\\}:[0-9]\\{2\\}\\)" end-input)
|
|
(match-string 1 end-input) ""))
|
|
(end-date-part (substring end-input 0 10))
|
|
(parsed (parse-time-string (concat end-date-part " 00:00")))
|
|
(ed (nth 3 parsed)) (em (nth 4 parsed)) (ey (nth 5 parsed))
|
|
(edow (format-time-string "%a" (encode-time 0 0 12 ed em ey))))
|
|
(if (string-empty-p end-time-part)
|
|
(format "<%04d-%02d-%02d %s %s>--<%04d-%02d-%02d %s>"
|
|
y m d dow start-time ey em ed edow)
|
|
(format "<%04d-%02d-%02d %s %s>--<%04d-%02d-%02d %s %s>"
|
|
y m d dow start-time ey em ed edow end-time-part))))
|
|
;; Same-day timed event with end time (HH:MM)
|
|
((and end-input (not (string-empty-p end-input)))
|
|
(format "<%04d-%02d-%02d %s %s-%s>" y m d dow start-time end-input))
|
|
;; Same-day timed event, no end time
|
|
(t
|
|
(format "<%04d-%02d-%02d %s %s>" y m d dow start-time)))))
|
|
|
|
(setq calfw-org-capture-template
|
|
'("c" "Calendar event" entry
|
|
(file "~/org/calendar_outbox.org")
|
|
"* %?\n%(my/calfw-capture-timestamp)\n"))
|
|
|
|
;; 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
|
|
"h" #'calfw-navi-previous-day-command
|
|
"l" #'calfw-navi-next-day-command
|
|
"j" #'calfw-navi-next-week-command
|
|
"k" #'calfw-navi-previous-week-command
|
|
"n" #'calfw-navi-next-week-command
|
|
"p" #'calfw-navi-previous-week-command
|
|
"t" #'calfw-navi-goto-today-command
|
|
"g" #'calfw-refresh-calendar-buffer
|
|
"W" #'calfw-change-view-week
|
|
"M" #'calfw-change-view-month
|
|
"D" #'calfw-change-view-day
|
|
"TAB" #'calfw-navi-next-item-command
|
|
"a" #'my/calfw-capture
|
|
"q" #'bury-buffer
|
|
"x" #'calfw-org-clean-exit
|
|
"RET" #'calfw-org-open-agenda-day
|
|
"<" #'calfw-navi-prev-view
|
|
">" #'calfw-navi-next-view)
|
|
|
|
;; Sort periods (same-day events from org-caldav active-range format)
|
|
(defun my/calfw-event-time-int (evt)
|
|
"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)
|
|
"Sort periods sublist in RESULT by start-time."
|
|
(mapcar (lambda (item)
|
|
(if (and (listp item) (eq 'periods (car item)))
|
|
(cons 'periods
|
|
(sort (copy-sequence (cdr item))
|
|
(lambda (a b)
|
|
(let ((ta (my/calfw-event-time-int a))
|
|
(tb (my/calfw-event-time-int b)))
|
|
(cond ((and ta tb) (< ta tb))
|
|
(ta t)
|
|
(t nil))))))
|
|
item))
|
|
result))
|
|
|
|
(defun my/calfw-sorted-file-source (name file color)
|
|
"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)
|
|
:color (calfw-source-color base)
|
|
:data (lambda (begin end)
|
|
(my/calfw-sort-periods
|
|
(funcall (calfw-source-data base) begin end))))))
|
|
|
|
(defun my/calfw-sorter (x y)
|
|
(let* ((ex (get-text-property 0 'cfw:event x))
|
|
(ey (get-text-property 0 'cfw:event y))
|
|
(ta (or (and ex (my/calfw-event-time-int ex))
|
|
(get-text-property 0 'time-of-day x)))
|
|
(tb (or (and ey (my/calfw-event-time-int ey))
|
|
(get-text-property 0 'time-of-day y))))
|
|
(cond ((and ta tb) (< ta tb)) (ta t) (tb nil) (t (string-lessp x y)))))
|
|
|
|
(defun my/open-calendar ()
|
|
"Open calfw with org-agenda + CalDAV sources (Suky, Klara, Family)."
|
|
(interactive)
|
|
(condition-case err
|
|
(let* ((cd (expand-file-name "~/org/caldav/"))
|
|
(sources (delq nil
|
|
(list
|
|
(calfw-org-create-source nil "Agenda" "SeaGreen4")
|
|
(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 "Klara" (concat cd "klara.org") "Gold"))
|
|
(when (file-exists-p (concat cd "family.org"))
|
|
(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))
|
|
(calfw-org-open-calendar))))
|
|
|
|
(map! :leader "o C" #'my/open-calendar))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; EXTENSIONS — envrc, embark, wgrep, kubel
|
|
;;; ============================================================
|
|
|
|
(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))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; 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)
|
|
|
|
(use-package! vundo
|
|
:commands vundo
|
|
:config (setq vundo-glyph-alist vundo-unicode-symbols))
|
|
|
|
(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)))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; BIBLIOGRAPHY — citar + biblio + org-cite
|
|
;;; ============================================================
|
|
;; Global .bib: ~/org/references.bib (always available)
|
|
;; Per-project: add #+BIBLIOGRAPHY: ./refs.bib in org file header
|
|
;;
|
|
;; Workflow:
|
|
;; SPC B n → create new .bib (prompted for path)
|
|
;; SPC B s → search CrossRef → in results press 'i' to save to .bib
|
|
;; SPC B i → insert citation in org file
|
|
;; Export: org-cite + CSL auto-formats references
|
|
;;
|
|
;; Per-project setup: put .dir-locals.el in project root:
|
|
;; ((org-mode . ((citar-bibliography . ("./refs.bib")))))
|
|
|
|
;; Global bibliography paths (setq! required for Doom's custom setter)
|
|
(after! citar
|
|
(setq! citar-bibliography '("~/org/references.bib"))
|
|
(setq! citar-notes-paths '("~/org/notes/"))
|
|
(setq! citar-library-paths '("~/org/papers/"))
|
|
|
|
;; Auto-create global .bib if missing
|
|
(let ((bib (expand-file-name "~/org/references.bib")))
|
|
(unless (file-exists-p bib)
|
|
(make-directory (file-name-directory bib) t)
|
|
(with-temp-file bib (insert "% Global bibliography\n\n")))))
|
|
|
|
;; org-cite: CSL processor (no BibLaTeX toolchain needed)
|
|
(after! oc
|
|
(require 'citeproc)
|
|
(setq org-cite-global-bibliography '("~/org/references.bib")
|
|
org-cite-export-processors '((latex csl)
|
|
(html csl)
|
|
(t csl))))
|
|
|
|
;; biblio.el: polite CrossRef access (required by their API policy)
|
|
(setq biblio-crossref-user-email-address "martin@sukany.cz")
|
|
|
|
;; BibTeX editing defaults
|
|
(setq bibtex-dialect 'biblatex
|
|
bibtex-autokey-year-length 4
|
|
bibtex-autokey-name-year-separator ""
|
|
bibtex-autokey-year-title-separator "-"
|
|
bibtex-autokey-titleword-length 5
|
|
bibtex-autokey-titlewords 3)
|
|
|
|
;; Helper: create new .bib file for a project
|
|
(defun my/bib-new ()
|
|
"Create a new .bib file at prompted path and open it."
|
|
(interactive)
|
|
(let ((path (read-file-name "New .bib file: " default-directory nil nil "refs.bib")))
|
|
(make-directory (file-name-directory path) t)
|
|
(unless (file-exists-p path)
|
|
(with-temp-file path
|
|
(insert (format "%% Bibliography: %s\n%% Add entries via SPC B s (search) or SPC B m (manual)\n\n"
|
|
(file-name-nondirectory path)))))
|
|
(find-file path)
|
|
(message "Opened %s — search online (SPC B s) or add manual entry (SPC B m)" path)))
|
|
|
|
;; Helper: import from DOI → appends to currently open .bib or global
|
|
(defun my/bib-import-doi ()
|
|
"Fetch BibTeX for a DOI and insert at point (open a .bib file first)."
|
|
(interactive)
|
|
(require 'biblio)
|
|
(require 'biblio-doi)
|
|
(let ((doi (read-string "DOI: ")))
|
|
(biblio-doi-insert-bibtex doi)))
|
|
|
|
;; Helper: insert a manual BibTeX entry template
|
|
(defun my/bib-insert-manual ()
|
|
"Insert a BibTeX entry template at point for manual editing."
|
|
(interactive)
|
|
(let ((type (completing-read "Entry type: "
|
|
'("article" "book" "inproceedings" "misc" "online"
|
|
"techreport" "thesis" "manual") nil t)))
|
|
(insert (pcase type
|
|
("article"
|
|
"@article{KEY,\n author = {},\n title = {},\n journal = {},\n year = {},\n volume = {},\n pages = {},\n doi = {},\n}\n")
|
|
("book"
|
|
"@book{KEY,\n author = {},\n title = {},\n publisher = {},\n year = {},\n isbn = {},\n}\n")
|
|
("inproceedings"
|
|
"@inproceedings{KEY,\n author = {},\n title = {},\n booktitle = {},\n year = {},\n pages = {},\n doi = {},\n}\n")
|
|
("misc"
|
|
"@misc{KEY,\n author = {},\n title = {},\n year = {},\n url = {},\n note = {},\n}\n")
|
|
("online"
|
|
"@online{KEY,\n author = {},\n title = {},\n url = {},\n urldate = {},\n year = {},\n}\n")
|
|
("techreport"
|
|
"@techreport{KEY,\n author = {},\n title = {},\n institution = {},\n year = {},\n number = {},\n}\n")
|
|
("thesis"
|
|
"@thesis{KEY,\n author = {},\n title = {},\n school = {},\n year = {},\n type = {phdthesis},\n}\n")
|
|
("manual"
|
|
"@manual{KEY,\n title = {},\n organization = {},\n year = {},\n url = {},\n}\n")))
|
|
;; Position cursor at KEY for immediate editing
|
|
(search-backward "KEY" nil t)
|
|
(message "Fill in KEY and fields. KEY = unique citation identifier (e.g. burns2016).")))
|
|
|
|
;; biblio-lookup is the universal search command (prompts for backend)
|
|
;; crossref-lookup, arxiv-lookup, dblp-lookup are backend-specific
|
|
(map! :leader
|
|
(:prefix ("B" . "bibliography")
|
|
:desc "Browse references" "b" #'citar-open
|
|
:desc "Insert citation [cite:]" "i" #'citar-insert-citation
|
|
:desc "Open notes for reference" "n" #'citar-open-notes
|
|
:desc "Refresh bibliography" "r" #'citar-refresh
|
|
:desc "New .bib file" "N" #'my/bib-new
|
|
:desc "Search online" "s" (cmd! (require 'biblio) (call-interactively #'biblio-lookup))
|
|
:desc "Import from DOI" "D" #'my/bib-import-doi
|
|
:desc "Insert manual entry" "m" #'my/bib-insert-manual
|
|
:desc "Open global .bib" "f" (cmd! (find-file (expand-file-name "~/org/references.bib")))))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; 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))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; GRAMMAR CHECK — LanguageTool
|
|
;;; ============================================================
|
|
|
|
(after! langtool
|
|
(setq langtool-language-tool-jar
|
|
(expand-file-name "~/languagetool/languagetool-commandline.jar")
|
|
langtool-default-language "cs"
|
|
langtool-mother-tongue "cs"))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; KEYBINDINGS — central reference
|
|
;;; ============================================================
|
|
;;; Standalone bindings (package-independent, safe to define top-level).
|
|
;;
|
|
;; The following bindings are defined near their packages (load-order sensitive):
|
|
;; SPC o r — elfeed (elfeed section)
|
|
;; SPC o n/N — org-noter (org-noter section)
|
|
;; SPC o C — calendar/calfw (calfw section)
|
|
;; SPC j k/K — link-hint (link-hint section)
|
|
;; SPC t o — olivetti (olivetti section)
|
|
;; SPC z +/= — zoom in/out (keybindings section below)
|
|
;; SPC o M — Matrix/ement (Matrix section)
|
|
;; SPC B b/i — bibliography/citar (bibliography section)
|
|
;; SPC s q/Q — org-ql (org-ql section)
|
|
;; SPC | di| ci| vi| — table cell objects (evil-org section)
|
|
|
|
(map! :leader
|
|
(:prefix ("z" . "zoom")
|
|
: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))
|
|
|
|
(map! :leader :desc "Elfeed" "o r" #'elfeed)
|
|
(map! :leader
|
|
(:prefix ("t" . "toggle")
|
|
:desc "Speech ON" "s" #'my/emacspeak-on
|
|
:desc "Speech OFF" "S" #'my/emacspeak-off))
|
|
(map! :leader
|
|
(:prefix ("h" . "help")
|
|
:desc "Describe bindings (buffer-local)" "B" #'describe-bindings))
|
|
(map! :leader "o k" #'kubel)
|
|
(map! :leader "s e" #'iedit-mode)
|
|
(map! :leader "u" #'vundo)
|
|
(map! :leader "n r u" #'org-roam-ui-mode)
|
|
(map! :leader
|
|
"t g" #'langtool-check
|
|
"t G" #'langtool-check-done)
|
|
(map! :leader "o c" #'my/org-caldav-sync)
|
|
|
|
;; gls
|
|
(setq insert-directory-program "gls")
|
|
|