798 lines
29 KiB
EmacsLisp
798 lines
29 KiB
EmacsLisp
;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
|
|
|
|
;;; ============================================================
|
|
;;; USER IDENTITY
|
|
;;; ============================================================
|
|
|
|
(setq user-full-name "Martin Sukany"
|
|
user-mail-address "martin@sukany.cz")
|
|
|
|
|
|
;;; ============================================================
|
|
;;; THEME & FONT
|
|
;;; ============================================================
|
|
|
|
(setq doom-theme 'modus-vivendi-deuteranopia
|
|
doom-font (font-spec :family "JetBrains Mono" :size 14)
|
|
doom-variable-pitch-font nil)
|
|
|
|
(setq display-line-numbers-type t)
|
|
|
|
|
|
;;; ============================================================
|
|
;;; UI & DISPLAY
|
|
;;; ============================================================
|
|
|
|
(setq doom-modeline-refresh-rate 1.0)
|
|
|
|
(setq which-key-idle-delay 0.8
|
|
which-key-idle-secondary-delay 0.05)
|
|
|
|
;; Centered cursor mode — disabled: conflicts with macOS Zoom focus follower
|
|
(use-package! centered-cursor-mode
|
|
:config
|
|
(setq ccm-vpos-init 0.5)
|
|
;; (global-centered-cursor-mode +1) ; uncomment to enable
|
|
)
|
|
|
|
|
|
;;; ============================================================
|
|
;;; MACOS / PLATFORM
|
|
;;; ============================================================
|
|
|
|
(setq mouse-autoselect-window t
|
|
focus-follows-mouse t
|
|
select-enable-clipboard t
|
|
select-enable-primary t
|
|
inhibit-splash-screen t)
|
|
|
|
;; PATH: add MacTeX binaries
|
|
(setenv "PATH" (concat "/Library/TeX/texbin:" (getenv "PATH")))
|
|
(add-to-list 'exec-path "/Library/TeX/texbin")
|
|
|
|
;; macOS clipboard integration via pbcopy/pbpaste (works in terminal Emacs too)
|
|
(defun my/pbcopy (text &optional _push)
|
|
"Send TEXT to the macOS clipboard using pbcopy."
|
|
(let ((process-connection-type nil))
|
|
(let ((proc (start-process "pbcopy" "*pbcopy*" "pbcopy")))
|
|
(process-send-string proc text)
|
|
(process-send-eof proc))))
|
|
|
|
(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 (y/d/c go to system)
|
|
(after! evil
|
|
(setq evil-want-clipboard t))
|
|
|
|
;; macOS Zoom accessibility — cancel persp-mode's 2.5s cache timer after startup
|
|
;; (reduces unnecessary redraws that cause Zoom to jump)
|
|
(run-with-timer 3 nil
|
|
(lambda ()
|
|
(when (and (boundp 'persp-frame-buffer-predicate-buffer-list-cache--timer)
|
|
(timerp persp-frame-buffer-predicate-buffer-list-cache--timer))
|
|
(cancel-timer persp-frame-buffer-predicate-buffer-list-cache--timer)
|
|
(setq persp-frame-buffer-predicate-buffer-list-cache--timer nil)
|
|
(message "persp-mode 2.5s cache timer cancelled for Zoom accessibility"))))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; MACOS GUI — FIXES
|
|
;;; ============================================================
|
|
|
|
;; Fix A: Ensure dashboard buffer starts in normal state (required for SPC leader)
|
|
(after! evil
|
|
(evil-set-initial-state '+doom-dashboard-mode 'normal))
|
|
|
|
;; Fix B: 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))
|
|
|
|
;; Fix C: SPC as leader key in GUI Emacs
|
|
;; In GUI mode, general-override-mode may not be active, so evil's default
|
|
;; SPC binding (evil-forward-char) takes precedence over Doom's leader key.
|
|
;; Force-enable general-override-mode and re-apply SPC → doom-leader-map.
|
|
(after! (evil general)
|
|
(general-override-mode +1))
|
|
|
|
(defun my/fix-gui-leader-key (&optional _frame)
|
|
"Bind SPC to `doom-leader-map' in evil normal/motion states.
|
|
Called for the initial GUI frame and any subsequent frames."
|
|
(when (and (display-graphic-p)
|
|
(boundp 'doom-leader-map)
|
|
(boundp 'evil-normal-state-map))
|
|
(define-key evil-normal-state-map (kbd "SPC") doom-leader-map)
|
|
(define-key evil-motion-state-map (kbd "SPC") doom-leader-map)))
|
|
|
|
;; Apply on every new GUI frame (covers emacsclient -c too)
|
|
(add-hook 'after-make-frame-functions #'my/fix-gui-leader-key)
|
|
;; Apply for the initial frame after Doom finishes init
|
|
(add-hook 'doom-after-init-hook #'my/fix-gui-leader-key)
|
|
|
|
|
|
;;; ============================================================
|
|
;;; PERFORMANCE & GC
|
|
;;; ============================================================
|
|
|
|
(setq gc-cons-threshold (* 100 1024 1024) ; 100 MB
|
|
gc-cons-percentage 0.6)
|
|
|
|
;; GCMH — Doom's GC manager; increase idle delay to reduce redraws
|
|
(after! gcmh
|
|
(setq gcmh-idle-delay 'auto
|
|
gcmh-auto-idle-delay-factor 20
|
|
gcmh-high-cons-threshold (* 200 1024 1024))) ; 200 MB
|
|
|
|
(add-hook 'focus-out-hook #'garbage-collect)
|
|
|
|
;; Auto-save all buffers on idle (replaces noisy #file# autosave)
|
|
(setq auto-save-default nil)
|
|
(defun my/save-all-buffers () (save-some-buffers t))
|
|
(run-with-idle-timer 10 t #'my/save-all-buffers)
|
|
|
|
;; !!! WARNING: TLS verification disabled globally !!!
|
|
;; Required for self-signed certs on local services (ai.apps.sukany.cz etc.)
|
|
(setq gnutls-verify-error nil
|
|
gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG MODE — CORE
|
|
;;; ============================================================
|
|
|
|
(after! org
|
|
(require 'ox-hugo)
|
|
|
|
(setq org-directory "~/org/")
|
|
(setq org-default-notes-file (expand-file-name "inbox.org" org-directory))
|
|
|
|
;; Helper: return absolute path to a file inside org-directory
|
|
(defun ms/org-file (name)
|
|
"Return absolute path to NAME inside `org-directory`."
|
|
(expand-file-name name org-directory))
|
|
|
|
(setq org-todo-keywords
|
|
'((sequence "TODO(t)" "NEXT(n)" "WAIT(w@/!)" "|" "DONE(d!)" "CANCELLED(c@)")))
|
|
|
|
(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)
|
|
|
|
;; Return path to project.org in current Projectile project, if it exists
|
|
(defun my/project-org-file ()
|
|
"Return path to ./project.org in current Projectile project, if it exists."
|
|
(when-let ((root (projectile-project-root)))
|
|
(let ((f (expand-file-name "project.org" root)))
|
|
(when (file-exists-p f) f))))
|
|
|
|
;; Update all dynamic blocks before export
|
|
(add-hook 'org-export-before-processing-hook
|
|
(lambda (_backend) (org-update-all-dblocks)))
|
|
|
|
;; Restore window layout after capture quit
|
|
(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))))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; 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
|
|
;;; ============================================================
|
|
|
|
;; Count data columns in an Org table line
|
|
(defun my/org-count-table-columns (line)
|
|
"Count the number of data columns in Org table LINE."
|
|
(length (cl-remove-if
|
|
(lambda (s) (string-match-p "^-*$" (string-trim s)))
|
|
(cdr (butlast (split-string line "|"))))))
|
|
|
|
;; Generate tabularx column spec: first column left-aligned, rest Y (auto-width)
|
|
(defun my/org-table-attr-latex-spec (ncols)
|
|
"Return tabularx column spec for NCOLS columns: first l, rest Y."
|
|
(concat "l" (make-string (max 0 (1- ncols)) ?Y)))
|
|
|
|
;; Automatically insert #+ATTR_LATEX tabularx before tables on LaTeX export
|
|
(defun my/org-auto-tabularx (backend)
|
|
"Insert #+ATTR_LATEX tabularx before each table when exporting to LaTeX."
|
|
(when (org-export-derived-backend-p backend 'latex)
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(while (not (eobp))
|
|
(cond
|
|
((looking-at "^|")
|
|
(let ((prev-line (save-excursion
|
|
(forward-line -1)
|
|
(buffer-substring-no-properties
|
|
(line-beginning-position) (line-end-position)))))
|
|
(when (not (string-match-p "^|" prev-line))
|
|
(when (not (string-match-p "^#\\+ATTR_LATEX" prev-line))
|
|
(let* ((table-line (buffer-substring-no-properties
|
|
(line-beginning-position) (line-end-position)))
|
|
(ncols (my/org-count-table-columns table-line))
|
|
(spec (my/org-table-attr-latex-spec ncols))
|
|
(attr (format "#+ATTR_LATEX: :environment tabularx :width \\textwidth :align %s\n"
|
|
spec)))
|
|
(when (> ncols 0)
|
|
(insert attr)))))
|
|
(forward-line))
|
|
(t
|
|
(forward-line))))))))
|
|
|
|
(add-hook 'org-export-before-processing-hook #'my/org-auto-tabularx)
|
|
|
|
;; Optional: enable booktabs style (horizontal rules in tables)
|
|
;; (setq org-latex-tables-booktabs t)
|
|
|
|
|
|
;;; ============================================================
|
|
;;; ORG MODE — CUSTOM BEHAVIOR
|
|
;;; ============================================================
|
|
|
|
;; Org agenda: position cursor at task name (after TODO keyword and priority)
|
|
;; Works with n/p (or j/k in evil mode) — skips TODO keyword and [#A] priority.
|
|
(defun my/org-agenda-goto-task-name (&rest _)
|
|
"Move cursor to the task name on the current org-agenda line.
|
|
Skips past the TODO keyword and optional priority indicator [#A]."
|
|
(when (get-text-property (line-beginning-position) 'org-hd-marker)
|
|
(beginning-of-line)
|
|
(let* ((bol (point))
|
|
(eol (line-end-position))
|
|
(todo-end nil)
|
|
(pos bol))
|
|
;; Find end of TODO keyword by face (org-todo or org-agenda-done)
|
|
(while (< pos eol)
|
|
(let* ((face (get-text-property pos 'face))
|
|
(next (or (next-single-property-change pos 'face nil eol) eol)))
|
|
(when (and face
|
|
(or (and (symbolp face)
|
|
(memq face '(org-todo org-agenda-done)))
|
|
(and (listp face)
|
|
(cl-intersection face '(org-todo org-agenda-done)))))
|
|
(setq todo-end next))
|
|
(setq pos next)))
|
|
;; Move past TODO keyword and optional priority [#X]
|
|
(when todo-end
|
|
(goto-char todo-end)
|
|
(skip-chars-forward " \t")
|
|
(when (looking-at "\\[#.\\][ \t]+")
|
|
(goto-char (match-end 0)))))))
|
|
|
|
(advice-add 'org-agenda-next-line :after #'my/org-agenda-goto-task-name)
|
|
(advice-add 'org-agenda-previous-line :after #'my/org-agenda-goto-task-name)
|
|
|
|
|
|
;;; ============================================================
|
|
;;; GPTEL — AI INTEGRATION (OpenWebUI / OpenRouter)
|
|
;;; ============================================================
|
|
|
|
(use-package! gptel
|
|
:config
|
|
;; 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: prefer gpt-5-mini, fall back to first available
|
|
(let* ((models (my/openwebui-models))
|
|
(preferred "openai/gpt-5-mini"))
|
|
(setq gptel-model (if (member preferred models) preferred (car models))))
|
|
|
|
;; 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)))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; COMPLETION — CORFU + CAPE
|
|
;;; ============================================================
|
|
|
|
(after! corfu
|
|
(setq corfu-auto t
|
|
corfu-auto-delay 0.15
|
|
corfu-auto-prefix 2
|
|
corfu-cycle t
|
|
corfu-preselect 'prompt
|
|
corfu-quit-no-match 'separator
|
|
corfu-preview-current nil)
|
|
(global-corfu-mode))
|
|
|
|
;; Cape: additional completion-at-point sources
|
|
(use-package! cape
|
|
:after corfu
|
|
:config
|
|
(defun martin/cape-capf-setup ()
|
|
"Set up cape completion sources for prog-mode and text-mode."
|
|
(add-to-list 'completion-at-point-functions #'cape-dabbrev 0) ; words from buffers
|
|
(add-to-list 'completion-at-point-functions #'cape-file 0) ; file paths
|
|
(add-to-list 'completion-at-point-functions #'cape-keyword 0) ; language keywords
|
|
(add-to-list 'completion-at-point-functions #'cape-elisp-symbol 0))
|
|
(add-hook 'prog-mode-hook #'martin/cape-capf-setup)
|
|
(add-hook 'text-mode-hook #'martin/cape-capf-setup))
|
|
|
|
;; Corfu popup in terminal (iTerm2 / SSH / tmux)
|
|
(use-package! corfu-terminal
|
|
:when (not (display-graphic-p))
|
|
:after corfu
|
|
:config
|
|
(corfu-terminal-mode +1))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; EMAIL — MU4E
|
|
;;; ============================================================
|
|
|
|
(add-to-list 'load-path
|
|
(expand-file-name "/opt/homebrew/opt/mu/share/emacs/site-lisp/mu/mu4e"))
|
|
|
|
(after! mu4e
|
|
(setq mu4e-maildir "~/.mail"
|
|
mu4e-get-mail-command "mbsync personal"
|
|
mu4e-update-interval 300
|
|
mu4e-change-filenames-when-moving t
|
|
mu4e-view-show-images t
|
|
mu4e-sent-folder "/personal/Sent"
|
|
mu4e-drafts-folder "/personal/Drafts"
|
|
mu4e-trash-folder "/personal/Trash"
|
|
mu4e-refile-folder "/personal/Archive"
|
|
mu4e-headers-show-threads t
|
|
mu4e-headers-include-related t
|
|
mu4e-use-fancy-chars t
|
|
mu4e-headers-mark-for-thread t
|
|
mu4e-headers-fields '((:human-date . 12)
|
|
(:flags . 6)
|
|
(:from . 22)
|
|
(:subject))))
|
|
|
|
(after! mu4e
|
|
(setq sendmail-program "msmtp"
|
|
message-send-mail-function #'message-send-mail-with-sendmail
|
|
mail-specify-envelope-from t
|
|
message-sendmail-envelope-from 'header))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; RSS — ELFEED
|
|
;;; ============================================================
|
|
|
|
(map! :leader :desc "Elfeed" "o r" #'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"
|
|
projectile-indexing-method 'alien))
|
|
|
|
;; Disable VC and Projectile over TRAMP — main cause of hangs
|
|
(setq vc-ignore-dir-regexp
|
|
(format "%s\\|%s" vc-ignore-dir-regexp tramp-file-name-regexp))
|
|
|
|
(defadvice projectile-project-root (around ignore-remote first activate)
|
|
(unless (file-remote-p default-directory) ad-do-it))
|
|
|
|
(setq remote-file-name-inhibit-cache nil
|
|
tramp-verbose 1)
|
|
|
|
|
|
;;; ============================================================
|
|
;;; DIRED
|
|
;;; ============================================================
|
|
|
|
(after! dired
|
|
(put 'dired-find-alternate-file 'disabled nil)
|
|
(map! :map dired-mode-map
|
|
"RET" #'dired-find-alternate-file
|
|
"^" #'dired-up-directory))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; PROJECTILE
|
|
;;; ============================================================
|
|
|
|
(after! projectile
|
|
(setq projectile-enable-caching nil
|
|
projectile-indexing-method 'alien)
|
|
(when (executable-find "fd")
|
|
(setq projectile-generic-command
|
|
"fd . -0 --type f --hidden --follow --exclude .git --color=never")))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; PYTHON
|
|
;;; ============================================================
|
|
|
|
(setq python-shell-interpreter "python3")
|
|
(after! org
|
|
(setq org-babel-python-command "python3")
|
|
(require 'ob-python))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; 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)
|
|
;; Hard inhibit: when non-nil, Emacspeak server will not start/restart
|
|
(defvar my/emacspeak-inhibit-server t)
|
|
|
|
(defun my/emacspeak--ensure-loaded ()
|
|
"Load Emacspeak once, safely, without breaking Doom startup."
|
|
(unless my/emacspeak-loaded
|
|
(setq my/emacspeak-loaded t)
|
|
(setq emacspeak-directory my/emacspeak-dir)
|
|
(load-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 ; OFF: do nothing, don't restart
|
|
(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)"))
|
|
|
|
(map! :leader
|
|
(:prefix ("t" . "toggle")
|
|
:desc "Speech ON" "s" #'my/emacspeak-on
|
|
:desc "Speech OFF" "S" #'my/emacspeak-off))
|
|
|
|
(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))
|
|
|
|
;; Apply global default speech rate after TTS init/restart
|
|
(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 TEXT SCALING (SPC z)
|
|
;;; ============================================================
|
|
;; Uses `default-text-scale` for true global scaling (face remapping on all
|
|
;; frames). Unlike `text-scale-mode` this affects every buffer, minibuffer,
|
|
;; popup, which-key, corfu, and transient menu uniformly.
|
|
|
|
(use-package! default-text-scale
|
|
:config
|
|
(setq default-text-scale-amount 10)) ; 10 units = 1pt
|
|
|
|
(defvar my/zoom-total-delta 0
|
|
"Cumulative face-height delta applied by global zoom (in face units).")
|
|
|
|
(defvar my/zoom-saved-delta nil
|
|
"Saved delta before last reset, used by `my/zoom-restore'.")
|
|
|
|
(defvar my/zoom-min-height 80
|
|
"Minimum face height (in units, 80 = 8pt). Zoom out stops here.")
|
|
|
|
(defun my/zoom--current-height ()
|
|
"Return the current default face height including zoom delta."
|
|
(face-attribute 'default :height))
|
|
|
|
(defun my/zoom--msg ()
|
|
"Display current effective font size in pt."
|
|
(let ((h (my/zoom--current-height)))
|
|
(message "Zoom: %s pt (delta %+d)"
|
|
(/ h 10)
|
|
my/zoom-total-delta)))
|
|
|
|
(defun my/zoom-in ()
|
|
"Increase global font size by 1 pt."
|
|
(interactive)
|
|
(default-text-scale-increase)
|
|
(cl-incf my/zoom-total-delta default-text-scale-amount)
|
|
(my/zoom--msg))
|
|
|
|
(defun my/zoom-out ()
|
|
"Decrease global font size by 1 pt (respects minimum)."
|
|
(interactive)
|
|
(let ((new-height (- (my/zoom--current-height) default-text-scale-amount)))
|
|
(if (< new-height my/zoom-min-height)
|
|
(message "Zoom: already at minimum (%d pt)" (/ (my/zoom--current-height) 10))
|
|
(default-text-scale-decrease)
|
|
(cl-decf my/zoom-total-delta default-text-scale-amount)
|
|
(my/zoom--msg))))
|
|
|
|
(defun my/zoom-reset ()
|
|
"Reset zoom to default size. Saves current delta for restore."
|
|
(interactive)
|
|
(if (= my/zoom-total-delta 0)
|
|
(message "Zoom: already at default size")
|
|
(setq my/zoom-saved-delta my/zoom-total-delta)
|
|
(default-text-scale-reset)
|
|
(setq my/zoom-total-delta 0)
|
|
(message "Zoom: reset to default (saved %+d for restore)"
|
|
my/zoom-saved-delta)))
|
|
|
|
(defun my/zoom-restore ()
|
|
"Restore the zoom level saved before last reset."
|
|
(interactive)
|
|
(if (null my/zoom-saved-delta)
|
|
(message "Zoom: nothing to restore")
|
|
(default-text-scale-adjust my/zoom-saved-delta)
|
|
(setq my/zoom-total-delta my/zoom-saved-delta)
|
|
(setq my/zoom-saved-delta nil)
|
|
(my/zoom--msg)))
|
|
|
|
(map! :leader
|
|
(:prefix ("z" . "zoom")
|
|
:desc "Zoom in" "+" #'my/zoom-in
|
|
:desc "Zoom in" "=" #'my/zoom-in
|
|
:desc "Zoom out" "-" #'my/zoom-out
|
|
:desc "Reset zoom" "0" #'my/zoom-reset
|
|
:desc "Restore zoom" "z" #'my/zoom-restore))
|
|
|
|
|
|
;;; ============================================================
|
|
;;; KEYBINDINGS
|
|
;;; ============================================================
|
|
|
|
(map! :leader
|
|
(:prefix ("h" . "help")
|
|
:desc "Describe bindings (buffer-local)" "B" #'describe-bindings))
|