diff options
| author | cashmere <cashmere@autistici.org> | 2026-07-11 08:50:14 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-07-11 08:50:14 +0200 |
| commit | c74c3526fe719ff8127d17f44b67a33772adc334 (patch) | |
| tree | 7b05d6cad835f8704430ac38c4f22358601fe846 /config.org | |
| parent | c6579ddf761aaa1c922213310dc1f46347ea06ca (diff) | |
| download | emacs-c74c3526fe719ff8127d17f44b67a33772adc334.tar.gz | |
2026-07-11T08:50:16+02:00
Diffstat (limited to 'config.org')
| -rw-r--r-- | config.org | 395 |
1 files changed, 240 insertions, 155 deletions
@@ -33,10 +33,6 @@ * local packages -Per-host development overrides. Any package registered in the gitignored =local.el= is loaded from a local checkout on =load-path= instead of its remote source, so I can hack on it in place (for example =kitty-graphics.el= on its =testing= branch) while =config.org= stays identical across every machine. No per-package recipe wrapper is needed: an =:around= advice on the elpaca =:ensure= normalizer rewrites the recipe to =nil= for anything in =my/local-packages=, so the =use-package= forms downstream use the plain =:ensure (:host github :repo "...")= they always have and =local.el= overrides. - -=local.el= is loaded inside =eval-and-compile= so the advice sees the same =my/local-packages= whether =config.el= runs as source or native-compiled. After editing =local.el=, re-evaluate the file (or restart) so the new override takes effect. A minimal =local.el= looks like: - #+begin_example ;;; -*- lexical-binding: t -*- (setq my/local-packages @@ -53,6 +49,8 @@ Set per-host in the gitignored `local.el'.") (dolist (entry my/local-packages) (add-to-list 'load-path (expand-file-name (cdr entry)))) +(add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory)) + (defun my/elpaca-skip-local (orig name _keyword args) (if (assq name my/local-packages) nil @@ -138,8 +136,8 @@ Set per-host in the gitignored `local.el'.") (scroll-bar-mode -1) (add-to-list 'default-frame-alist '(vertical-scroll-bars . nil)) (add-to-list 'default-frame-alist '(horizontal-scroll-bars . nil)) - ;; (set-frame-parameter (selected-frame) 'alpha-background 80) - ;; (add-to-list 'default-frame-alist '(alpha-background . 80)) + (set-frame-parameter (selected-frame) 'alpha-background 80) + (add-to-list 'default-frame-alist '(alpha-background . 80)) (global-hl-line-mode 1) ;; Highlight the current line (add-hook 'org-mode-hook (lambda () (setq-local global-hl-line-mode nil))) ;; hl-line repaints wipe kitty-graphics scaled headings (add-hook 'markdown-mode-hook (lambda () (setq-local global-hl-line-mode nil))) ;; same conflict for kitty-graphics markdown headings @@ -288,6 +286,8 @@ Selects the compilation window so the cursor lands in it." (setq insert-directory-program gls))))) #+end_src +* async + #+begin_src emacs-lisp (use-package async :ensure t) #+end_src @@ -781,7 +781,8 @@ Defined early so later =with-eval-after-load 'general= callbacks find =my-leader (my-leader "aa" '(agent-shell :wk "start") "at" '(agent-shell-toggle :wk "toggle") - "am" '(agent-shell-help-menu :wk "open session"))) + "am" '(agent-shell-help-menu :wk "open session") + "ar" '(agent-shell-send-region :wk "send region"))) :config ;; Runs only on first invocation — not at Emacs startup. @@ -814,7 +815,27 @@ let's give agent-shell another try #+begin_src emacs-lisp (defun my/agent-shell-autoscroll (&rest _) (dolist (win (get-buffer-window-list (current-buffer) 'visible)) - (set-window-point win (point-max)))) + (when (>= (window-point win) + (save-excursion (goto-char (point-max)) (point))) + (set-window-point win (point-max))))) + +(defun my/agent-shell-display-buffer-rules () + '(("\\*Kimi\\(?:\\*\\|:[^*]*\\*\\)" + (display-buffer-in-side-window) + (side . bottom) + (slot . 0) + (window-height . 0.33) + (dedicated . t) + (window-parameters . ((no-other-window . nil) + (no-delete-other-windows . nil)))) + ("\\*agent-shell-diff\\*" + (display-buffer-in-side-window) + (side . right) + (slot . 1) + (window-width . 0.5) + (dedicated . t) + (window-parameters . ((no-other-window . nil) + (no-delete-other-windows . nil)))))) (use-package agent-shell :ensure t @@ -823,10 +844,51 @@ let's give agent-shell another try ("M-RET" . shell-maker-submit)) :config (advice-add 'shell-maker-submit :after #'my/agent-shell-autoscroll) - (setq agent-shell-anthropic-claude-environment - (agent-shell-make-environment-variables - "ANTHROPIC_BASE_URL" "https://api.minimax.io/anthropic" - "ANTHROPIC_AUTH_TOKEN" (password-store-get "minimax")))) + (dolist (rule (my/agent-shell-display-buffer-rules)) + (add-to-list 'display-buffer-alist rule)) + (setq agent-shell-tool-use-expand-by-default nil) + (setq agent-shell-thought-process-expand-by-default nil) + (setq agent-shell-user-message-expand-by-default nil) + (setq agent-shell-diff-delta-command + (when (executable-find "delta") + '("delta" "--no-gitconfig" "--color-only"))) + + ;; Work around agent-shell's `window-system' guard so clipboard images + ;; work in terminal Emacs (`emacs -nw'). The underlying save routine + ;; already shells out to wl-paste/xclip/pngpaste/powershell, none of + ;; which require a GUI frame. + (defun my/agent-shell-send-clipboard-image-in-terminal (orig-fn &optional pick-shell) + "Call ORIG-FN, but allow `agent-shell-send-clipboard-image' in terminal Emacs." + (if (window-system) + (funcall orig-fn pick-shell) + (let* ((screenshots-dir (agent-shell--dot-subdir "screenshots")) + (image-path (agent-shell--save-clipboard-image + :destination-dir screenshots-dir)) + (shell-buffer (when pick-shell + (agent-shell--read-shell-buffer + :prompt "Send image to shell: ")))) + (agent-shell-insert + :text (agent-shell--get-files-context :files (list image-path)) + :shell-buffer shell-buffer)))) + (advice-add 'agent-shell-send-clipboard-image :around + #'my/agent-shell-send-clipboard-image-in-terminal) + + ;; Also let C-y / `agent-shell-yank-dwim' try the clipboard image first + ;; in terminal Emacs before falling back to plain text yank. + (defun my/agent-shell-yank-dwim-in-terminal (orig-fn &optional arg) + "Call ORIG-FN, but try clipboard image first in terminal Emacs." + (if (window-system) + (funcall orig-fn arg) + (if-let* ((screenshots-dir (agent-shell--dot-subdir "screenshots")) + (image-path (agent-shell--save-clipboard-image + :destination-dir screenshots-dir + :no-error t))) + (agent-shell-insert + :text (agent-shell--get-files-context :files (list image-path)) + :shell-buffer (agent-shell--shell-buffer)) + (funcall orig-fn arg)))) + (advice-add 'agent-shell-yank-dwim :around + #'my/agent-shell-yank-dwim-in-terminal)) #+end_src * eldoc @@ -1063,6 +1125,10 @@ Only activates when running in a Kitty terminal (not in GUI Emacs). (kitty-gfx-shr-fit-height 20) :hook (dired-mode . kitty-gfx-dired-auto-preview-mode) :config + (setq kitty-gfx-enable-browser t + kitty-gfx-casty-program "~/projects/casty/bin/casty.js" + kitty-gfx-casty-chrome "helium-browser") + (kitty-graphics-setup)) #+end_src @@ -1477,55 +1543,22 @@ Skips capture tasks and projects." *** org-capture helpers #+begin_src emacs-lisp -(defun my/org-capture-timeblock-to-journal () - "Capture function: jump to today's journal for a timeblock." - (require 'denote) +(defun my/org-capture-to-journal (heading) (require 'denote-journal) - (let* ((today (format-time-string "%Y-%m-%d")) - (journal-files (denote-directory-files "journal")) - (today-journal (seq-find - (lambda (file) - (string-match-p today (file-name-nondirectory file))) - journal-files))) - (if today-journal - (find-file today-journal) - (denote-journal-new-or-existing-entry)) - (goto-char (point-max)) - (insert "\n* Timeblock\n"))) + (denote-journal-new-or-existing-entry) + (goto-char (point-max)) + (insert heading) + (current-buffer)) + +(defun my/org-capture-timeblock-to-journal () + (my/org-capture-to-journal "\n* Timeblock\n")) (defun my/org-capture-recurring-to-journal () - "Capture function: jump to today's journal for a recurring task." - (require 'denote) - (require 'denote-journal) - (let* ((today (format-time-string "%Y-%m-%d")) - (journal-files (denote-directory-files "journal")) - (today-journal (seq-find - (lambda (file) - (string-match-p today (file-name-nondirectory file))) - journal-files))) - (if today-journal - (find-file today-journal) - (denote-journal-new-or-existing-entry)) - (goto-char (point-max)) - (insert "\n* Recurring\n"))) + (my/org-capture-to-journal "\n* Recurring\n")) (defun my/org-capture-meeting-to-journal () - "Capture function: jump to today's journal for a meeting note." - (require 'denote) - (require 'denote-journal) - (let* ((today (format-time-string "%Y-%m-%d")) - (journal-files (denote-directory-files "journal")) - (today-journal (seq-find - (lambda (file) - (string-match-p today (file-name-nondirectory file))) - journal-files))) - (if today-journal - (find-file today-journal) - (denote-journal-new-or-existing-entry)) - (goto-char (point-max)) - (insert "\n* Meeting: "))) + (my/org-capture-to-journal "\n* Meeting: ")) -;; capture templates are merged in the org-capture section below #+end_src @@ -1541,48 +1574,42 @@ Skips capture tasks and projects." *** org-capture combined with denote -#+begin_src emacs-lisp -(defun my/org-capture-denote-deadline () +#+begin_src emacs-lisp +(defun my/org-capture-task-to-journal () + (let* ((context (read-string "Task: ")) + (todo-state (completing-read "TODO state: " + '("ACTIVE" "NEXT" "TODO" "WAIT")))) + (my/org-capture-to-journal (format "\n* %s %s\n\n" todo-state context)))) + +(defun my/org-capture-deadline-to-journal () (let* ((context (read-string "Task with deadline: ")) - (deadline (org-read-date t nil nil "Deadline: ")) - (file-name (denote nil '("agenda")))) - (find-file file-name) - (goto-char (point-min)) - (forward-line 4) - (insert (format "\n* %s\nDEADLINE: <%s>\n\n" context deadline)) - (current-buffer))) + (deadline (org-read-date t nil nil "Deadline: "))) + (my/org-capture-to-journal (format "\n* %s\nDEADLINE: <%s>\n\n" context deadline)))) -(defun my/org-capture-denote-scheduled () +(defun my/org-capture-scheduled-to-journal () + "Capture a scheduled task into today's denote-journal file." (let* ((context (read-string "Scheduled task: ")) - (schedule (org-read-date t nil nil "Schedule: ")) - (file-name (denote nil '("agenda")))) - (find-file file-name) - (goto-char (point-min)) - (forward-line 4) - (insert (format "\n* %s\nSCHEDULED: <%s>\n\n" context schedule)) - (current-buffer))) + (schedule (org-read-date t nil nil "Schedule: "))) + (my/org-capture-to-journal (format "\n* %s\nSCHEDULED: <%s>\n\n" context schedule)))) -(defun my/org-capture-denote-task () - (let* ((context (read-string "Task: ")) - (todo-state (completing-read "TODO state: " - '("ACTIVE" "NEXT" "TODO" "WAIT"))) - (file-name (denote nil '("agenda")))) - (find-file file-name) - (goto-char (point-min)) - (forward-line 4) - (insert (format "\n* %s %s\n\n" todo-state context)) - (point))) +(defun my/org-capture-float () + (interactive) + (select-frame + (make-frame '((title . "org-capture") + (width . 160) + (height . 14)))) + (org-capture)) (with-eval-after-load 'org (setq org-capture-templates '(("t" "Task" plain - (function my/org-capture-denote-task) + (function my/org-capture-task-to-journal) "" :immediate-finish t :jump-to-captured t) ("d" "Deadline" plain - (function my/org-capture-denote-deadline) + (function my/org-capture-deadline-to-journal) "" :immediate-finish t :jump-to-captured t) ("s" "Scheduled" plain - (function my/org-capture-denote-scheduled) + (function my/org-capture-scheduled-to-journal) "" :immediate-finish t :jump-to-captured t) ("b" "Timeblock" plain (function my/org-capture-timeblock-to-journal) @@ -2350,6 +2377,34 @@ Timers that expired while Emacs was closed fire immediately." denote-merge-region-markdown-fenced-block)) #+end_src +** denote-solo + +Auto switch silos (different workspaces/folders) based on preconfigured directories. + +#+begin_src emacs-lisp +(use-package denote-solo + :ensure (:host github :repo "pavlo/denote-solo") + :after denote + :config + (setq denote-solo-directories + '(("org" . "~/org") + ("garden" . "~/garden")) + denote-solo-display-modeline nil) + (denote-solo-mode 1) + + (defun my/denote-solo-sync-directory (&rest _) + (when-let* ((solo denote-solo--current-solo) + (path (denote-solo--directory-for-name solo))) + (setq-default denote-directory path) + (dolist (buf (buffer-list)) + (with-current-buffer buf + (when (local-variable-p 'denote-directory) + (setq denote-directory path)))))) + + (advice-add 'denote-solo-switch :after #'my/denote-solo-sync-directory)) +#+end_src + + * which-key #+begin_src emacs-lisp @@ -3434,9 +3489,44 @@ Native =undo-redo= (via =evil-undo-system='undo-redo=); just raise the undo limi * modelines +** display-time + +#+begin_src emacs-lisp +(display-time-mode 1) +#+end_src + ** doom-modeline #+begin_src emacs-lisp +(defvar-local my/doom-modeline--buffer-title nil) + +(defun my/doom-modeline--update-title-cache () + "Update the cached #+title for the current buffer." + (setq-local my/doom-modeline--buffer-title + (cadar (org-collect-keywords '("TITLE"))))) + +(defun my/doom-modeline--buffer-title () + "Return the cached #+title, computing it if necessary." + (unless (local-variable-p 'my/doom-modeline--buffer-title) + (my/doom-modeline--update-title-cache)) + my/doom-modeline--buffer-title) + +(defun my/doom-modeline-set-buffer-title (&rest _) + "Override doom-modeline's cached file name with the Org #+title. +If no TITLE keyword is found, leave doom-modeline's default name." + (when-let* ((title (my/doom-modeline--buffer-title))) + (setq doom-modeline--buffer-file-name + (propertize title + 'face 'doom-modeline-buffer-file + 'mouse-face 'mode-line-highlight + 'help-echo (concat (or buffer-file-truename (buffer-name)) + "\nmouse-1: Previous buffer\nmouse-3: Next buffer") + 'local-map mode-line-buffer-identification-keymap)))) + +(defun my/doom-modeline--invalidate-title-cache () + "Invalidate the cached title so it gets recomputed on next update." + (kill-local-variable 'my/doom-modeline--buffer-title)) + (use-package doom-modeline :ensure t :init (doom-modeline-mode 1) @@ -3448,7 +3538,9 @@ Native =undo-redo= (via =evil-undo-system='undo-redo=); just raise the undo limi doom-modeline-buffer-file-name-style 'relative-to-project doom-modeline-buffer-encoding nil doom-modeline-time t - doom-modeline-time-live-icon t)) + doom-modeline-time-live-icon t) + (advice-add #'doom-modeline-update-buffer-file-name :after #'my/doom-modeline-set-buffer-title) + (add-hook 'before-save-hook #'my/doom-modeline--invalidate-title-cache)) #+end_src * soph @@ -4153,6 +4245,7 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts." "dl" '(denote-link-or-create t :wk "Link Note") "dn" '(denote t :wk "Create a new note") "dr" '(denote-rename-file t :wk "Rename Note") + "ds" '(denote-solo-switch :wk "switch silo") "dtl" '(tmr-list-timers :wk "list timer") "dtt" '(tmr :wk "set timer") @@ -4207,6 +4300,7 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts." "os" '(my/snip-upload :wk "snip buffer/region") "oS" '(my/snip-upload-file :wk "snip file") "op" '(pass :wk "pass") + "ot" '(ghostel :wk "ghostel") "h" '(:ignore t :wk "help") "hm" '(describe-mode :wk "mode") @@ -4279,17 +4373,19 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts." (call-interactively 'flash-evil-jump))))) (general-def '(normal visual operator) 'override + :predicate '(not (derived-mode-p 'mu4e-main-mode 'mu4e-headers-mode + 'mu4e-view-mode 'mu4e-compose-mode)) "s" 'my/s-key-dispatch) (general-def 'normal 'override "K" 'my/eldoc-and-jump - "'d" 'flycheck-next-error - ";d" 'flycheck-previous-error + "]d" 'flycheck-next-error + "[d" 'flycheck-previous-error "]c" 'diff-hl-next-hunk "[c" 'diff-hl-previous-hunk - "'b" 'switch-to-next-buffer - ";b" 'switch-to-prev-buffer + "]b" 'switch-to-next-buffer + "[b" 'switch-to-prev-buffer "]t" 'tab-next "[t" 'tab-previous "P" 'consult-yank-from-kill-ring @@ -4325,6 +4421,7 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts." "mC" '(org-capture :wk "capture") "mc" '(:wk "set" :ignore) + "mk" '(kitty-gfx-org-heading-sizes :wk "kgfx headlines") "mcd" '(org-deadline :wk "deadline") "mcs" '(org-schedule :wk "schedule") "mce" '(org-set-effort :wk "effort") @@ -5027,7 +5124,6 @@ opening another file in same project does not re-notify." (autoload 'garden-fleet "garden-fleet" nil t) (autoload 'garden-connect "garden-connect" nil t) (autoload 'garden-connect-pick "garden-connect" nil t) - ;; (autoload 'garden-player "garden-player" nil t) (autoload 'garden-publish "garden-publish" nil t) (autoload 'garden-publish-all "garden-publish" nil t) (autoload 'garden-publish-wiki "garden-publish" nil t) @@ -5036,34 +5132,13 @@ opening another file in same project does not re-notify." (autoload 'denote-capf-setup "denote-capf" nil t) (with-eval-after-load 'denote-capf (setq denote-capf-directories '("~/org/"))) - (add-hook 'org-mode-hook #'denote-capf-setup)) -#+end_src - -** garden-player - -=garden-player= (=M-x garden-player=, buffer =*navi*=) is a terminal Navidrome client: it streams through a background =mpv= over a unix socket, renders a TUI with =vui=, and shows album art inline through the kitty graphics protocol. It opens on your starred library; press =u= to flip the song list to the live up-next queue and =u= again to flip back. - -The buffer installs =garden-player-map= as a vim-like keymap (bindings live in =garden-player.el=, this is just the reference): + (add-hook 'org-mode-hook #'denote-capf-setup) -| key | action | -|-----+--------------------------------------------------------------| -| =SPC= | play / pause | -| =h= | previous track | -| =l= | next track | -| =j= | restart the current song from the beginning | -| =k= | cycle repeat mode: off → repeat-all → repeat-one | -| =s= | reshuffle the queue | -| =n= | search the library and play the pick next (no playlist made) | -| =N= | search the library and append the pick to the end | -| =/= | same as =N= | -| =u= | toggle between the starred library and the up-next queue | -| =f= | favorite the current song | -| =r= | radio: queue songs similar to the current one | -| =g= | reload the starred library | -| =q= | quit | -| =RET= | play the song on the current row | - -Interactive commands: =garden-player= opens the player; =garden-player-play-next= and =garden-player-enqueue= search and queue without touching server playlists; =garden-player-toggle-view= flips the list; =garden-player-repeat=, =garden-player-shuffle=, =garden-player-radio=, =garden-player-like=, =garden-player-refresh= and =garden-player-quit= back the matching keys. + (defun my/denote-capf-tab-in-insert () + (when (featurep 'evil) + (evil-local-set-key 'insert (kbd "TAB") #'indent-for-tab-command))) + (add-hook 'org-mode-hook #'my/denote-capf-tab-in-insert)) +#+end_src * sops @@ -5104,52 +5179,62 @@ Interactive commands: =garden-player= opens the player; =garden-player-play-next #+begin_src emacs-lisp (use-package pretty-sha-path - :ensure t) + :ensure t + :config + (setopt global-pretty-sha-path-mode 't)) #+end_src - -* COMMENT empv +* pulsar-mode #+begin_src emacs-lisp -(use-package empv +(use-package pulsar :ensure t - :defer t :config - (setq empv-subsonic-url (password-store-get-field "navidrome" "url")) - (setq empv-subsonic-username (password-store-get-field "navidrome" "username")) - (setq empv-subsonic-password (password-store-get "navidrome")) - - (defun my/empv-subsonic-starred-songs () - "List every starred song in Navidrome and play the selected one." - (interactive) - (empv--subsonic-request - "getStarred2.view" - (empv--subsonic-result-handler "Starred song:"))) - - (defun my/empv-subsonic-starred-songs-urls () - "Return a list of playable URLs for every starred song." - (thread-last - (empv--subsonic-request "getStarred2.view") - (alist-get 'results) - (seq-filter (lambda (item) (eq (alist-get 'kind item) 'song))) - (mapcar #'empv--subsonic-item-extract-url))) + (setq pulsar-pulse t) + (setq pulsar-delay 0.025) + (setq pulsar-iterations 10) + (setq pulsar-face 'evil-ex-lazy-highlight) + (setq pulsar-tty-color "white") - (defun my/empv-subsonic-play-all-starred-songs () - "Clear the playlist and play every starred song." - (interactive) - (if-let ((urls (my/empv-subsonic-starred-songs-urls))) - (empv-play urls) - (message "No starred songs found"))) + (let ((orig-face-background (symbol-function 'face-background))) + (defun my/pulsar--create-pulse (locus face) + "Like `pulsar--create-pulse' but with a smarter TTY colour fallback." + (let ((common-fn (lambda (locus face) + (let ((pulse-flag t) + (pulse-delay pulsar-delay) + (pulse-iterations pulsar-iterations) + (overlay (make-overlay (car locus) (cdr locus)))) + (overlay-put overlay 'pulse-delete t) + (overlay-put overlay 'window (frame-selected-window)) + (pulse-momentary-highlight-overlay overlay face))))) + (if (display-graphic-p) + (funcall common-fn locus face) + (cl-letf (((symbol-function 'face-background) + (lambda (f &optional frame inherit) + (let ((bg (funcall orig-face-background f frame inherit))) + (cond + ((and (eq f 'default) (null bg)) + (or (frame-parameter frame 'background-color) + (if (eq (frame-parameter frame 'background-mode) 'light) + "white" + "black"))) + ((null bg) pulsar-tty-color) + (t bg)))))) + (funcall common-fn locus face)))))) + (advice-add 'pulsar--create-pulse :override #'my/pulsar--create-pulse) - (defun my/empv-subsonic-enqueue-all-starred-songs () - "Append every starred song to the current playlist." - (interactive) - (if-let ((urls (my/empv-subsonic-starred-songs-urls))) - (empv-enqueue urls) - (message "No starred songs found"))) + (add-to-list 'pulsar-pulse-functions 'evil-scroll-down) + (add-to-list 'pulsar-pulse-functions 'flymake-goto-next-error) + (add-to-list 'pulsar-pulse-functions 'flymake-goto-prev-error) + (add-to-list 'pulsar-pulse-functions 'evil-yank) + (add-to-list 'pulsar-pulse-functions 'evil-yank-line) + (add-to-list 'pulsar-pulse-functions 'evil-delete) + (add-to-list 'pulsar-pulse-functions 'evil-delete-line) + (add-to-list 'pulsar-pulse-functions 'evil-jump-item) + (add-to-list 'pulsar-pulse-functions 'diff-hl-next-hunk) + (add-to-list 'pulsar-pulse-functions 'diff-hl-previous-hunk) - (with-eval-after-load 'embark - (empv-embark-initialize-extra-actions))) + (pulsar-global-mode)) #+end_src * provide init |
