commit c0b2845152c10034030887c686f96eab8b3c7a94 from: mtmn date: Wed Jul 22 22:13:55 2026 UTC fmt, add fontaine and acme theme commit - 53b6364d5d04cee453e08bdb4300c821032b5a18 commit + c0b2845152c10034030887c686f96eab8b3c7a94 blob - b3d04c243e01df2c0d661a0edb46dbabb9d065af blob + a5eda36186d27caae165593cf5f2ad7607941b1e --- .gitignore +++ .gitignore @@ -13,3 +13,4 @@ /straight /undo-fu-session/ /*.elc +/fontaine-latest-state.eld blob - 3ead197b17b7252ef3b872d0955548377ea4a1e2 blob + e958d042b0a62b7c0ebfbc7fd4f7f7ace78b9b58 --- config.org +++ config.org @@ -2,7 +2,7 @@ :header-args:emacs-lisp: :tangle init.el :END: -#+title: Emacs Configuration +#+title: Literate Emacs configuration #+date: 2026-07-20 #+filetags: :emacs: @@ -14,7 +14,8 @@ ;; Crank GC and disable the file-name handler for the duration of startup; ;; both are restored to sane runtime values on `emacs-startup-hook' below. (setq bedrock--initial-gc-threshold gc-cons-threshold - bedrock--initial-file-name-handler-alist file-name-handler-alist + bedrock--initial-file-name-handler-alist + file-name-handler-alist gc-cons-threshold most-positive-fixnum gc-cons-percentage 0.6 file-name-handler-alist nil @@ -27,11 +28,11 @@ frame-resize-pixelwise t package-enable-at-startup nil straight-repository-branch "develop" - straight-check-for-modifications '(check-on-save find-when-checking) + straight-check-for-modifications + '(check-on-save find-when-checking) straight-use-package-by-default nil straight-vc-git-default-clone-depth 1 default-frame-alist '((fullscreen . maximized) - (font . "Go Mono-14") (internal-border-width . 0) (undecorated . t))) @@ -39,7 +40,8 @@ "Restore GC threshold, GC percentage, and `file-name-handler-alist'." (setq gc-cons-threshold (* 128 1024 1024) gc-cons-percentage 0.1 - file-name-handler-alist bedrock--initial-file-name-handler-alist)) + file-name-handler-alist + bedrock--initial-file-name-handler-alist)) (add-hook 'emacs-startup-hook #'bedrock--restore-startup-defaults) @@ -51,7 +53,8 @@ (defvar bootstrap-version) (let ((bootstrap-file - (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory)) + (expand-file-name "straight/repos/straight.el/bootstrap.el" + user-emacs-directory)) (bootstrap-version 6)) (unless (file-exists-p bootstrap-file) (with-current-buffer @@ -68,8 +71,9 @@ * Core settings #+begin_src emacs-lisp - (when (< emacs-major-version 29) - (error "Emacs configuration only works with Emacs 29 and newer; you have version %s" +(when (< emacs-major-version 30) + (error + "Emacs configuration only works with Emacs 30 and newer; you have version %s" emacs-major-version)) (setopt initial-major-mode 'fundamental-mode @@ -95,13 +99,6 @@ (menu-bar-mode -1) (scroll-bar-mode -1)) - ;; Use Go Mono everywhere. `default' is set directly here; `fixed-pitch' - ;; and `variable-pitch' must be pointed at it explicitly so that e.g. - ;; `variable-pitch-mode' in org-mode still renders in Go Mono. - (set-face-attribute 'default nil :family "Go Mono" :height 140) - (set-face-attribute 'fixed-pitch nil :family "Go Mono" :height 1.0) - (set-face-attribute 'variable-pitch nil :family "Go Mono" :height 1.0) - (unless (display-graphic-p) (xterm-mouse-mode 1)) @@ -128,7 +125,9 @@ #+begin_src emacs-lisp (defun bedrock--backup-file-name (fpath) "Return a backup path for FPATH, creating parent dirs if needed." - (let* ((backup-root-dir (concat user-emacs-directory "emacs-backup/")) + (let* + ((backup-root-dir + (concat user-emacs-directory "emacs-backup/")) (file-path (replace-regexp-in-string "[A-Za-z]:" "" fpath)) (backup-file-path (replace-regexp-in-string "//" "/" @@ -137,7 +136,9 @@ backup-file-path)) (setopt make-backup-file-name-function #'bedrock--backup-file-name) - (let ((auto-save-dir (expand-file-name "auto-saves/" "~/.cache/emacs")) + (let + ((auto-save-dir + (expand-file-name "auto-saves/" "~/.cache/emacs")) (lock-dir (expand-file-name "locks/" "~/.cache/emacs"))) (make-directory auto-save-dir t) (make-directory lock-dir t) @@ -145,49 +146,133 @@ lock-file-name-transforms `((".*" ,lock-dir t)))) #+end_src +* Fonts + +#+begin_src emacs-lisp + (use-package fontaine + :straight + t + :demand t + :bind ("C-c F" . fontaine-set-preset) + :init + (setq fontaine-latest-state-file + (locate-user-emacs-file "fontaine-latest-state.eld")) + :config + (setq fontaine-presets + '((comic-code + :default-family "Comic Code" + :default-height 120) + (comic-code-large + :default-family "Comic Code" + :default-height 170) + (codelia + :default-family "Codelia" + :default-height 130) + (droid-sans-mono + :default-family "Droid Sans Mono Dotted" + :default-height 130) + (fragment-mono + :default-family "Fragment Mono" + :default-height 130) + (fantasque-sans-mono + :default-family "Fantasque Sans Mono" + :default-height 140) + (go-mono + :default-family "Go Mono" + :default-height 120) + (inconsolata + :default-family "Inconsolata" + :default-weight medium + :default-height 120) + (input-mono + :default-family "Input Mono" + :default-height 120) + (ioskeley + :default-family "Ioskeley Mono Term" + :default-height 120) + (pragmatapro + :default-family "PragmataPro" + :default-height 130) + (0xproto + :default-family "0xProto" + :default-height 120) + (sudo + :default-family "Sudo" + :default-height 140) + (ttyp0 + :default-family "Ttyp0 OTB" + :default-height 130) + (t + :default-weight regular + :default-slant normal + :fixed-pitch-family nil + :variable-pitch-family nil + :bold-weight bold + :italic-slant italic))) + (defun bedrock--fontaine-sync-variable-pitch (&rest _) + (set-face-attribute 'variable-pitch nil + :family (face-attribute 'default :family) + :height 1.0)) + (add-hook 'fontaine-set-preset-hook + #'bedrock--fontaine-sync-variable-pitch) + (fontaine-mode 1) + (fontaine-set-preset + (or (fontaine-restore-latest-preset) 'go-mono))) +#+end_src + * UI #+begin_src emacs-lisp - (use-package modus-themes - :straight t - :demand t - :config - (load-theme 'modus-operandi t)) + (use-package modus-themes + :straight + t + :demand t + :config) - (use-package ef-themes - :straight t - :commands ef-themes-select) + (use-package ef-themes + :straight + t + :demand t + :config) - (use-package which-key - :straight t - :defer 1 - :config (which-key-mode)) + (use-package plan9-theme + :straight + t + :demand t + :config) - (use-package smart-mode-line - :straight t - :demand t - :custom - (sml/no-confirm-load-theme t) - (sml/theme 'respectful) - :config - (sml/setup)) + (use-package which-key + :straight + t + :defer 1 + :config (which-key-mode)) - (use-package minions - :straight t - :demand t - :config - (minions-mode)) + (use-package minions + :straight + t + :demand t + :config + (minions-mode)) + (use-package mood-line + :straight + t + :demand t + :config + (mood-line-mode)) + + ;; Lets `:diminish' hide minor-mode lighters from the mode line. (use-package diminish - :straight t + :straight + t :demand t) (add-to-list 'tab-bar-format 'tab-bar-format-align-right 'append) (add-to-list 'tab-bar-format 'tab-bar-format-global 'append) - (setopt display-time-format "%a %F %T" + (setopt display-time-format "%T" display-time-interval 1) - (display-time-mode 1) + (display-time-mode 0) #+end_src * Scrolling @@ -212,31 +297,37 @@ #+begin_src emacs-lisp (use-package lin - :straight t + :straight + t :hook ((eshell-mode . lin-mode) (dired-mode . lin-mode) (org-mode . lin-mode))) (use-package pulsar - :straight t + :straight + t :config (pulsar-global-mode)) (use-package diff-hl - :straight t + :straight + t :hook ((prog-mode . diff-hl-mode) (org-mode . diff-hl-mode)) :config (global-diff-hl-show-hunk-mouse-mode)) (use-package circadian - :straight (circadian :type git :host github :repo "guidoschmidt/circadian.el") + :straight + (circadian :type git :host github :repo + "guidoschmidt/circadian.el") :demand t :config (setopt calendar-latitude 48.1386 calendar-longitude 17.1053 - circadian-themes '((:sunrise . ef-orange) - (:sunset . modus-vivendi))) + circadian-themes '((:sunrise . plan9) + (:sunset . plan9))) + ;; (:sunset . ef-tritanopia-dark))) (circadian-setup)) #+end_src @@ -249,22 +340,26 @@ (keymap-set minibuffer-mode-map "TAB" #'minibuffer-complete) (use-package vertico - :straight t + :straight + t :init (vertico-mode)) (with-eval-after-load 'vertico (keymap-set vertico-map "M-DEL" #'vertico-directory-delete-word)) (use-package marginalia - :straight t + :straight + t :config (marginalia-mode)) (use-package orderless - :straight t + :straight + t :config (setopt completion-styles '(orderless))) (use-package consult - :straight t + :straight + t :bind (("C-x b" . consult-buffer) ("M-y" . consult-yank-pop) ("M-s r" . consult-ripgrep) @@ -280,16 +375,19 @@ (consult-narrow-key "<")) (use-package embark - :straight t + :straight + t :after consult :bind (("C-c a" . embark-act))) (use-package embark-consult - :straight t + :straight + t :after (embark consult)) (use-package corfu - :straight t + :straight + t :init (global-corfu-mode) :bind (:map corfu-map ("SPC" . corfu-insert-separator))) @@ -302,25 +400,29 @@ (corfu-popupinfo-mode)) (use-package corfu-terminal - :straight t + :straight + t :if (not (display-graphic-p)) :config (corfu-terminal-mode)) (use-package cape - :straight t + :straight + t :init (add-to-list 'completion-at-point-functions #'cape-dabbrev) (add-to-list 'completion-at-point-functions #'cape-file)) (use-package kind-icon - :straight t + :straight + t :if (display-graphic-p) :after corfu :config (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter)) (use-package wgrep - :straight t + :straight + t :config (setopt wgrep-auto-save-buffer t)) #+end_src @@ -328,7 +430,8 @@ #+begin_src emacs-lisp (use-package avy - :straight t + :straight + t :bind (("C-c j" . avy-goto-line) ("s-j" . avy-goto-char-timer)) :config @@ -341,14 +444,16 @@ (select-window (cdr (ring-ref avy-ring 0)))) t) - (setf (alist-get ?. avy-dispatch-alist) #'bedrock-avy-action-embark)) + (setf (alist-get ?. avy-dispatch-alist) + #'bedrock-avy-action-embark)) #+end_src * Evil #+begin_src emacs-lisp (use-package evil - :straight t + :straight + t :custom (evil-want-keybinding nil) (evil-respect-visual-line-mode t) @@ -360,22 +465,26 @@ (evil-set-initial-state 'eat-mode 'emacs)) (use-package evil-collection - :straight t + :straight + t :after evil :config (evil-collection-init)) (use-package evil-surround - :straight t + :straight + t :after evil :config (global-evil-surround-mode)) (use-package evil-commentary - :straight t + :straight + t :after evil :config (evil-commentary-mode)) (use-package evil-org - :straight t + :straight + t :after org :hook (org-mode . evil-org-mode) :config @@ -387,7 +496,8 @@ #+begin_src emacs-lisp (use-package undo-fu - :straight t + :straight + t :custom (undo-fu-allow-undo-in-region t) (undo-fu-undo-command 'undo-fu-only-undo) @@ -397,7 +507,8 @@ ("M-/" . undo-fu-only-redo-all))) (use-package undo-fu-session - :straight t + :straight + t :custom (undo-fu-session-incompatible-files '("\\.gpg$")) (undo-fu-session-directory @@ -406,7 +517,8 @@ (undo-fu-session-global-mode)) (use-package vundo - :straight t + :straight + t :bind ("C-c u" . vundo)) (with-eval-after-load 'evil @@ -418,7 +530,8 @@ #+begin_src emacs-lisp (use-package helpful - :straight t + :straight + t :bind (("C-h f" . helpful-callable) ("C-h v" . helpful-variable) ("C-h k" . helpful-key) @@ -430,7 +543,8 @@ #+begin_src emacs-lisp (use-package eshell - :straight (:type built-in) + :straight + (:type built-in) :init (defun bedrock-setup-eshell () "Bind `C-r' to `consult-history' in `eshell-mode'." @@ -438,7 +552,8 @@ :hook (eshell-mode . bedrock-setup-eshell)) (use-package eat - :straight t + :straight + t :after eshell :custom ;; A universally-installed TERM, so shells eat spawns never look up the @@ -451,7 +566,8 @@ #+begin_src emacs-lisp (use-package tramp - :straight (:type built-in) + :straight + (:type built-in) :defer t :custom (tramp-default-method "ssh") @@ -465,7 +581,8 @@ #+begin_src emacs-lisp (use-package treesit-auto - :straight t + :straight + t :demand t :custom (treesit-auto-install t) @@ -476,11 +593,13 @@ (add-hook 'prog-mode-hook #'electric-pair-mode) (use-package project - :straight (:type built-in) + :straight + (:type built-in) :custom (project-mode-line t)) (use-package magit - :straight t + :straight + t :bind (("C-x g" . magit-status))) #+end_src @@ -488,39 +607,54 @@ #+begin_src emacs-lisp (use-package yaml-mode - :straight t + :straight + t :mode "\\.ya?ml\\'") (use-package json-mode - :straight t + :straight + t :mode "\\.json\\'") (use-package nix-mode - :straight t + :straight + t :mode "\\.nix\\'") (use-package go-mode - :straight t + :straight + t :mode "\\.go\\'") (use-package zig-mode - :straight t + :straight + t :mode "\\.zig\\'") (use-package rust-mode - :straight t + :straight + t :mode "\\.rs\\'") (use-package elixir-mode - :straight t + :straight + t :mode ("\\.exs?\\'" "mix\\.lock\\'")) + + (use-package markdown-mode + :straight + t + :mode "\\.md\\'" + :hook (markdown-mode . visual-line-mode)) + #+end_src * LSP #+begin_src emacs-lisp (use-package eglot - :straight (:type built-in) + :straight + (:type built-in) :custom (eglot-send-changes-idle-time 0.1) (eglot-extend-to-xref t) @@ -538,7 +672,8 @@ #+begin_src emacs-lisp (use-package tempel - :straight t + :straight + t :bind (("M-*" . tempel-insert) ("M-+" . tempel-complete) :map tempel-map @@ -548,7 +683,8 @@ :init (defun tempel-setup-capf () "Add `tempel-expand' to `completion-at-point-functions' locally." - (add-hook 'completion-at-point-functions #'tempel-expand -1 'local)) + (add-hook 'completion-at-point-functions #'tempel-expand -1 + 'local)) (add-hook 'prog-mode-hook #'tempel-setup-capf) (add-hook 'text-mode-hook #'tempel-setup-capf)) #+end_src @@ -557,7 +693,8 @@ #+begin_src emacs-lisp (use-package slime - :straight t + :straight + t :custom (inferior-lisp-program "sbcl") (slime-lisp-implementations '((sbcl ("sbcl")))) @@ -574,7 +711,8 @@ (setopt denote-directory (expand-file-name "~/.nb/notes")) (use-package denote - :straight t + :straight + t :custom (denote-file-type 'org) (denote-known-keywords '("daily" "comp" "music" "misc")) @@ -583,14 +721,16 @@ (denote-rename-buffer-mode)) (use-package denote-journal - :straight t + :straight + t :after denote :custom (denote-journal-keyword "journal") (denote-journal-file-type 'org)) (use-package denote-org - :straight t + :straight + t :after denote) #+end_src @@ -599,21 +739,25 @@ #+begin_src emacs-lisp (setopt org-directory (expand-file-name "~/.nb/notes") org-agenda-files (list org-directory) - org-default-notes-file (expand-file-name "todo.org" org-directory)) + org-default-notes-file + (expand-file-name "todo.org" org-directory)) (setopt org-refile-targets '((org-agenda-files :maxlevel . 3)) org-refile-use-outline-path 'file org-outline-path-complete-in-steps nil) (setopt org-todo-keywords - '((sequence "TODO(t)" "WAITING(w@/!)" "STARTED(s!)" "|" "DONE(d!)" "OBSOLETE(o@)")) + '((sequence "TODO(t)" "WAITING(w@/!)" "STARTED(s!)" "|" + "DONE(d!)" "OBSOLETE(o@)")) org-log-done 'time org-log-into-drawer t) (setopt org-capture-templates - '(("t" "task" entry (file+headline org-default-notes-file "Inbox") + '(("t" "task" entry + (file+headline org-default-notes-file "Inbox") "* TODO %?\n%U\n%i") - ("j" "journal" plain (file denote-journal-path-to-new-or-existing-entry) + ("j" "journal" plain + (file denote-journal-path-to-new-or-existing-entry) "%?\n%U\n%i"))) (setopt org-agenda-start-with-log-mode 'clockcheck @@ -635,12 +779,14 @@ org-startup-with-inline-images t org-hide-emphasis-markers t) - (setopt org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id) + (setopt org-id-link-to-org-use-id + 'create-if-interactive-and-no-custom-id) (setopt org-export-with-smart-quotes t) (use-package org - :straight (:type built-in) + :straight + (:type built-in) :hook ((org-mode . visual-line-mode) (org-mode . variable-pitch-mode)) :bind (:map global-map @@ -658,24 +804,18 @@ org-cycle-separator-lines 2)) (use-package org-appear - :straight t + :straight + t :hook (org-mode . org-appear-mode)) (use-package org-super-agenda - :straight t + :straight + t :after org :config (org-super-agenda-mode)) #+end_src -* Markdown - -#+begin_src emacs-lisp - (use-package markdown-mode - :straight t - :hook (markdown-mode . visual-line-mode)) -#+end_src - * Key bindings #+begin_src emacs-lisp @@ -717,7 +857,8 @@ :doc "Prefix map for denote commands.") (keymap-set global-map "C-c n" denote-prefix-map) (keymap-set denote-prefix-map "n" #'denote) - (keymap-set denote-prefix-map "j" #'denote-journal-new-or-existing-entry) + (keymap-set denote-prefix-map "j" + #'denote-journal-new-or-existing-entry) (keymap-set denote-prefix-map "l" #'denote-insert-link) (keymap-set denote-prefix-map "L" #'denote-add-links) (keymap-set denote-prefix-map "o" #'denote-open-or-create) @@ -758,7 +899,12 @@ (with-eval-after-load 'evil (evil-set-leader '(normal visual) (kbd "SPC")) - (evil-define-key '(normal visual) 'global (kbd "fh") #'consult-recent-file)) + (evil-define-key '(normal visual) 'global + (kbd "fh") #'consult-recent-file) + (evil-define-key '(normal visual) 'global + (kbd "ff") #'project-find-file) + (evil-define-key '(normal visual) 'global + (kbd "fs") #'save-buffer)) #+end_src * Personal extras @@ -769,18 +915,22 @@ save-interprogram-text-before-kill t) (use-package clipetty - :straight t + :straight + t :hook (after-init . clipetty-mode)) (with-eval-after-load 'eglot (add-to-list 'eglot-server-programs - '(purescript-mode . ("pnpx" "purescript-language-server" "--stdio"))) + '(purescript-mode + . ("pnpx" "purescript-language-server" "--stdio"))) (add-to-list 'eglot-server-programs - '((markdown-mode text-mode tex-mode typst-mode org-mode) + '((markdown-mode text-mode tex-mode typst-mode + org-mode) . ("harper-ls" "--stdio")))) (use-package zoom-window - :straight t + :straight + t :bind ("C-x z" . zoom-window-zoom)) #+end_src @@ -788,7 +938,8 @@ #+begin_src emacs-lisp (use-package tmr - :straight t + :straight + t :bind (("C-c t c" . tmr) ("C-c t l" . tmr-tabulated-view) ("C-c t k" . tmr-remove) @@ -799,7 +950,8 @@ LLMs in Emacs. A single backend talks to Ollama Cloud over its OpenAI-compatible endpoint; the API key is read from `auth-source' -(default `~/.authinfo.gpg'), so no secret lives in this repository. On +(default `~/.authinfo.gpg'), so no secret lives in this repository +. On top of that we register web, filesystem and Emacs tools, and a set of presets (`@research', `@agent', per-model `@qwen' …) selectable per request. @@ -831,8 +983,11 @@ response beginning and end positions; we ignore both." (defun mine/ollama-api-key () "Return the Ollama Cloud API key stored in auth-source. Expects a line in `auth-sources' for host ollama.com, user apikey." - (or (auth-source-pick-first-password :host "ollama.com" :user "apikey") - (user-error "No Ollama API key in auth-source for host ollama.com"))) + (or + (auth-source-pick-first-password :host "ollama.com" :user + "apikey") + (user-error + "No Ollama API key in auth-source for host ollama.com"))) (defun mine/ollama-api-post (path payload) "POST PAYLOAD (an alist) as JSON to Ollama API PATH, return parsed JSON. @@ -840,15 +995,20 @@ Signals an error on a non-2xx response. Runs synchron (let* ((url-request-method "POST") (url-request-extra-headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " (mine/ollama-api-key))))) - (url-request-data (encode-coding-string (json-encode payload) 'utf-8)) - (buf (url-retrieve-synchronously (concat "https://ollama.com" path) + ("Authorization" + . ,(concat "Bearer " (mine/ollama-api-key))))) + (url-request-data + (encode-coding-string (json-encode payload) 'utf-8)) + (buf (url-retrieve-synchronously + (concat "https://ollama.com" path) 'silent nil 60))) (unless buf (error "Ollama API request to %s timed out" path)) (unwind-protect (with-current-buffer buf (goto-char (point-min)) - (let ((status (and (re-search-forward "^HTTP/[0-9.]+ \\([0-9]+\\)" nil t) + (let ((status (and + (re-search-forward + "^HTTP/[0-9.]+ \\([0-9]+\\)" nil t) (string-to-number (match-string 1))))) (unless (re-search-forward "\n\n" nil t) (error "Malformed response from Ollama API")) @@ -857,7 +1017,8 @@ Signals an error on a non-2xx response. Runs synchron (json-key-type 'symbol) (body (json-read))) (when (and status (>= status 400)) - (error "Ollama API %s failed (HTTP %d): %s" path status + (error "Ollama API %s failed (HTTP %d): %s" path + status (or (alist-get 'error body) body))) body))) (kill-buffer buf)))) @@ -867,7 +1028,8 @@ Signals an error on a non-2xx response. Runs synchron #+begin_src emacs-lisp (use-package gptel - :straight (gptel :type git :host github :repo "karthink/gptel") + :straight + (gptel :type git :host github :repo "karthink/gptel") :demand t :bind (:map gptel-mode-map ("C-c RET" . gptel-send) @@ -880,11 +1042,16 @@ Signals an error on a non-2xx response. Runs synchron (gptel-use-header-line t) (gptel-org-ignore-elements '(property-drawer)) (gptel-directives - '((default . "You are a large language model living in Emacs and a helpful assistant. Respond concisely.") - (programming . "You are a careful programmer working from inside Emacs. Provide code, with minimal surrounding prose.") - (reviewing . "You are reviewing code or text in Emacs. Be terse, point out concrete issues, and suggest fixes.") - (literate . "You are helping with a literate configuration in an Org file. Prefer Org markup and explain only what is non-obvious.") - (chat . "You are a conversation partner. Respond concisely."))) + '((default + . "You are a large language model living in Emacs and a helpful assistant. Respond concisely.") + (programming + . "You are a careful programmer working from inside Emacs. Provide code, with minimal surrounding prose.") + (reviewing + . "You are reviewing code or text in Emacs. Be terse, point out concrete issues, and suggest fixes.") + (literate + . "You are helping with a literate configuration in an Org file. Prefer Org markup and explain only what is non-obvious.") + (chat + . "You are a conversation partner. Respond concisely."))) (gptel-use-tools t) (gptel-confirm-tool-calls 'auto) (gptel-include-tool-results 'auto) @@ -919,48 +1086,63 @@ Signals an error on a non-2xx response. Runs synchron (gptel-make-tool :name "web_search" :category "web" - :description (concat "Search the web and return a list of results, each with a " + :description (concat + "Search the web and return a list of results, each with a " "title, URL and a content snippet. Use this for current " "events or anything outside your training data, then call " "`web_fetch' on a promising URL to read it in full.") :args (list '(:name "query" :type string :description "The search query.") '(:name "max_results" :type integer :optional t - :description "How many results to return (1-10, default 5).")) + :description + "How many results to return (1-10, default 5).")) :function (lambda (query &optional max_results) (let* ((payload `(("query" . ,query) - ,@(when (and (integerp max_results) (> max_results 0)) + ,@(when + (and (integerp max_results) + (> max_results 0)) `(("max_results" . ,(min max_results 10)))))) - (results (alist-get 'results (mine/ollama-api-post "/api/web_search" payload)))) + (results + (alist-get 'results + (mine/ollama-api-post "/api/web_search" + payload)))) (if (null results) "No results found." (mapconcat (lambda (r) (format "## %s\n%s\n\n%s" - (alist-get 'title r) (alist-get 'url r) (alist-get 'content r))) + (alist-get 'title r) (alist-get 'url r) + (alist-get 'content r))) results "\n\n---\n\n"))))) (gptel-make-tool :name "web_fetch" :category "web" - :description "Fetch and return the main text content of a web page, given its URL." + :description + "Fetch and return the main text content of a web page, given its URL." :args (list '(:name "url" :type string - :description "The full URL of the page to fetch.")) + :description + "The full URL of the page to fetch.")) :function (lambda (url) - (let* ((resp (mine/ollama-api-post "/api/web_fetch" `(("url" . ,url)))) + (let* + ((resp + (mine/ollama-api-post "/api/web_fetch" `(("url" . ,url)))) (content (alist-get 'content resp))) (if (and (stringp content) (not (string-empty-p content))) - (format "# %s\n\n%s" (or (alist-get 'title resp) url) content) + (format "# %s\n\n%s" (or (alist-get 'title resp) url) + content) "Could not extract any content from that URL.")))) (gptel-make-tool :name "read_url" :category "web" - :description (concat "Fetch a URL directly (no API) and return its rendered text. " + :description (concat + "Fetch a URL directly (no API) and return its rendered text. " "A fallback for `web_fetch'.") - :args (list '(:name "url" :type string :description "The URL to read.")) + :args + (list '(:name "url" :type string :description "The URL to read.")) :function (lambda (url) (with-current-buffer (url-retrieve-synchronously url t t 30) @@ -977,7 +1159,8 @@ Signals an error on a non-2xx response. Runs synchron :category "emacs" :description "Return the full contents of a live Emacs buffer." :args (list '(:name "buffer" :type string - :description "The name of the buffer to read.")) + :description + "The name of the buffer to read.")) :function (lambda (buffer) (unless (buffer-live-p (get-buffer buffer)) @@ -990,7 +1173,8 @@ Signals an error on a non-2xx response. Runs synchron :category "filesystem" :description "Read and return the contents of a file on disk." :args (list '(:name "filepath" :type string - :description "Path to the file. Supports relative paths and ~.")) + :description + "Path to the file. Supports relative paths and ~.")) :function (lambda (filepath) (with-temp-buffer @@ -1002,24 +1186,31 @@ Signals an error on a non-2xx response. Runs synchron :category "filesystem" :description "List the entries in a directory." :args (list '(:name "directory" :type string - :description "The path to the directory to list.")) + :description + "The path to the directory to list.")) :function (lambda (directory) - (mapconcat #'identity (directory-files (expand-file-name directory)) "\n"))) + (mapconcat #'identity + (directory-files (expand-file-name directory)) "\n"))) (gptel-make-tool :name "run_command" :category "command" - :description "Execute a shell command and return its combined output." + :description + "Execute a shell command and return its combined output." :args (list '(:name "command" :type string - :description "The complete shell command to run.") + :description + "The complete shell command to run.") '(:name "working_dir" :type string :optional t - :description "Directory to run in (default: current).")) + :description + "Directory to run in (default: current).")) :function (lambda (command &optional working_dir) (with-temp-message (format "Executing: %s" command) (let ((default-directory - (if (and working_dir (not (string-empty-p working_dir))) + (if + (and working_dir + (not (string-empty-p working_dir))) (expand-file-name working_dir) default-directory))) (shell-command-to-string command)))) @@ -1029,7 +1220,8 @@ Signals an error on a non-2xx response. Runs synchron (gptel-make-tool :name "obscura_scrape" :category "web" - :description (concat "Scrape a web page with the Obscura headless browser " + :description (concat + "Scrape a web page with the Obscura headless browser " "(real JS rendering, stealth). Use this instead of " "`web_fetch'/`read_url' for JavaScript-heavy or " "bot-protected pages. Returns the page as markdown by " @@ -1038,20 +1230,29 @@ Signals an error on a non-2xx response. Runs synchron :args (list '(:name "url" :type string :description "The URL to scrape.") '(:name "format" :type string :optional t - :description "Output: markdown (default), text, html, or links.") + :description + "Output: markdown (default), text, html, or links.") '(:name "selector" :type string :optional t - :description "Optional CSS selector to wait for before extracting.")) + :description + "Optional CSS selector to wait for before extracting.")) :function (lambda (url &optional format selector) (if-let* ((exe (executable-find "obscura"))) (with-temp-message (format "Obscura scraping %s" url) (with-output-to-string - (apply #'call-process exe nil (list standard-output nil) nil + (apply #'call-process exe nil + (list standard-output nil) nil "fetch" url - "--dump" (if (member format '("text" "html" "links" "markdown")) - format "markdown") + "--dump" (if + (member format + '("text" "html" "links" + "markdown")) + format + "markdown") "--wait-until" "networkidle0" - (when (and selector (not (string-empty-p selector))) + (when + (and selector + (not (string-empty-p selector))) (list "--selector" selector))))) "Error: the `obscura' binary is not on PATH. Install it (https://github.com/h4ckf0r0day/obscura) or use `web_fetch'/`read_url' instead.")) :confirm t @@ -1066,8 +1267,10 @@ Signals an error on a non-2xx response. Runs synchron ;; Presets: apply with `C-c L p' or an `@name' cookie at the start of a prompt. (gptel-make-preset 'research - :description "Web research: search, fetch and scrape, then synthesise." - :system (concat "You are a research assistant working inside Emacs. Use the " + :description + "Web research: search, fetch and scrape, then synthesise." + :system (concat + "You are a research assistant working inside Emacs. Use the " "web tools to gather up-to-date information before answering: " "`web_search' to find sources, `web_fetch'/`read_url' to read " "them, and `obscura_scrape' for JavaScript-heavy or protected " @@ -1076,19 +1279,23 @@ Signals an error on a non-2xx response. Runs synchron :confirm-tool-calls nil) (gptel-make-preset 'agent - :description "Coding agent: read files/buffers and run commands in Emacs." - :system (concat "You are a careful coding agent operating inside Emacs. Use " + :description + "Coding agent: read files/buffers and run commands in Emacs." + :system (concat + "You are a careful coding agent operating inside Emacs. Use " "the filesystem and buffer tools to inspect the project, and " "`run_command' to build, test or inspect the repo. Explain " "what you will do, then act. Prefer minimal, surgical changes.") - :tools '("read_file" "read_buffer" "list_directory" "run_command") + :tools + '("read_file" "read_buffer" "list_directory" "run_command") :confirm-tool-calls t) (gptel-make-preset 'full :description "Everything: web + filesystem + Emacs + commands." :system 'default :tools '("web_search" "web_fetch" "read_url" "obscura_scrape" - "read_file" "read_buffer" "list_directory" "run_command")) + "read_file" "read_buffer" "list_directory" + "run_command")) (gptel-make-preset 'chat :description "Plain conversation, no tools." @@ -1106,98 +1313,123 @@ Signals an error on a non-2xx response. Runs synchron (minimax . minimax-m3:cloud) (minimax2 . minimax-m2.7:cloud))) (gptel-make-preset (car spec) - :description (format "Use the %s model on Ollama Cloud." (cdr spec)) + :description + (format "Use the %s model on Ollama Cloud." (cdr spec)) :backend "Ollama Cloud" :model (cdr spec)))) ;; Bundled with gptel (same repo, already on `load-path'); no recipe needed. (use-package gptel-transient - :after gptel + :after + gptel :commands (gptel-menu gptel-system-prompt gptel-tools)) #+end_src * Literate programming -Tools for working with infrastructure and configuration files in a literate -way: Dockerfiles, Compose files, shell scripts, REST clients, and org-babel +Tools for working with infrastructure and configuration files in a +literate +way: Dockerfiles, Compose files, shell scripts, REST clients, and +org-babel sources so we can tangle and run code blocks from this very file. ** Languages and file modes #+begin_src emacs-lisp (use-package dockerfile-mode - :straight t + :straight + t :mode (("\\.Dockerfile\\'" . dockerfile-mode) ("/Dockerfile\\'" . dockerfile-mode))) (use-package docker - :straight t + :straight + t :commands (docker docker-compose)) (use-package nginx-mode - :straight t + :straight + t :mode "\\.nginx\\'") (use-package toml-mode - :straight t + :straight + t :mode "\\.toml\\'") (use-package ini-mode - :straight t + :straight + t :mode (("\\.ini\\'" . ini-mode) - ("/\\.env\\.\\(?:local\\|development\\|production\\)?\\'" . ini-mode))) + ("/\\.env\\.\\(?:local\\|development\\|production\\)?\\'" + . ini-mode))) (use-package restclient - :straight t + :straight + t :mode (("\\.http\\'" . restclient-mode) ("\\.rest\\'" . restclient-mode))) (use-package graphql-mode - :straight t + :straight + t :mode "\\.gql\\(?:s\\)?\\'") (use-package docker-compose-mode - :straight t - :mode (("docker-compose\\(?:\\..*\\)?\\.ya?ml\\'" . docker-compose-mode) - ("compose\\.ya?ml\\'" . docker-compose-mode))) + :straight + t + :mode + (("docker-compose\\(?:\\..*\\)?\\.ya?ml\\'" . docker-compose-mode) + ("compose\\.ya?ml\\'" + . docker-compose-mode))) #+end_src ** Org-babel languages -These give us executable Org source blocks for shell scripts, REST calls, -and inline LLM completions. `ob-async' lets long blocks (model downloads, -container pulls) run in the background. +These give us executable Org source blocks for shell scripts, REST +calls, +and inline LLM completions. `ob-async' lets long blocks (model + downloads, +container pulls) +run in the background. #+begin_src emacs-lisp (use-package ob-async - :straight t + :straight + t :after org) (use-package ob-restclient - :straight t + :straight + t :after org) (use-package ob-http - :straight t + :straight + t :after org) (use-package ob-graphql - :straight t + :straight + t :after org) (use-package ob-mermaid - :straight t + :straight + t :after org :config (setopt ob-mermaid-cli-path (or (executable-find "mmdc") "npx"))) (use-package ob-d2 - :straight t + :straight + t :after org) (use-package ob-llm - :straight t + :straight + t :after org :config ;; Send babel blocks through gptel rather than a hosted API directly. @@ -1205,7 +1437,8 @@ container pulls) run in the background. ob-llm-stream t)) (use-package ob-tmux - :straight t + :straight + t :after org :config (setopt ob-tmux-location-alist @@ -1213,16 +1446,23 @@ container pulls) run in the background. ("work" . "/var/tmp")))) #+end_src -Enable a useful default set of babel languages and configure evaluation -safety so we can run trusted devops blocks without per-block confirmation. +Enable a useful default set of babel languages and configure +evaluation +safety so we can run trusted devops blocks without per-block +confirmation. #+begin_src emacs-lisp (with-eval-after-load 'org (setopt org-babel-default-languages - '(emacs-lisp shell python docker dockerfile restclient http graphql mermaid d2 tmux llm)) + '(emacs-lisp shell python docker dockerfile restclient + http graphql mermaid d2 tmux llm)) (setopt org-confirm-babel-evaluate nil - org-babel-default-header-args:shell '((:results . "output") (:async . "yes")) - org-babel-default-header-args:tmux '((:results . "output") (:async . "yes")) - org-babel-default-header-args:restclient '((:results . "raw")) - org-babel-default-header-args:http '((:results . "raw")))) + org-babel-default-header-args:shell + '((:results . "output") (:async . "yes")) + org-babel-default-header-args:tmux + '((:results . "output") (:async . "yes")) + org-babel-default-header-args:restclient + '((:results . "raw")) + org-babel-default-header-args:http + '((:results . "raw")))) #+end_src blob - 736348b6f623f178e6de6dbcd8331734e36fa044 blob + 5790f308de1e06dbb180de0353aab35010b8d345 --- early-init.el +++ early-init.el @@ -3,7 +3,8 @@ ;; Crank GC and disable the file-name handler for the duration of startup; ;; both are restored to sane runtime values on `emacs-startup-hook' below. (setq bedrock--initial-gc-threshold gc-cons-threshold - bedrock--initial-file-name-handler-alist file-name-handler-alist + bedrock--initial-file-name-handler-alist + file-name-handler-alist gc-cons-threshold most-positive-fixnum gc-cons-percentage 0.6 file-name-handler-alist nil @@ -16,11 +17,11 @@ frame-resize-pixelwise t package-enable-at-startup nil straight-repository-branch "develop" - straight-check-for-modifications '(check-on-save find-when-checking) + straight-check-for-modifications + '(check-on-save find-when-checking) straight-use-package-by-default nil straight-vc-git-default-clone-depth 1 default-frame-alist '((fullscreen . maximized) - (font . "Go Mono-14") (internal-border-width . 0) (undecorated . t))) @@ -28,7 +29,8 @@ "Restore GC threshold, GC percentage, and `file-name-handler-alist'." (setq gc-cons-threshold (* 128 1024 1024) gc-cons-percentage 0.1 - file-name-handler-alist bedrock--initial-file-name-handler-alist)) + file-name-handler-alist + bedrock--initial-file-name-handler-alist)) (add-hook 'emacs-startup-hook #'bedrock--restore-startup-defaults) @@ -40,7 +42,8 @@ (defvar bootstrap-version) (let ((bootstrap-file - (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory)) + (expand-file-name "straight/repos/straight.el/bootstrap.el" + user-emacs-directory)) (bootstrap-version 6)) (unless (file-exists-p bootstrap-file) (with-current-buffer blob - 818c440582ccc4c765cfb3a11da03af6330409bb blob + 289e83347397d30fc667e9af53a23b451d1ba42b --- init.el +++ init.el @@ -1,60 +1,56 @@ -(when (< emacs-major-version 29) - (error "Emacs configuration only works with Emacs 29 and newer; you have version %s" - emacs-major-version)) +(when (< emacs-major-version 30) + (error + "Emacs configuration only works with Emacs 30 and newer; you have version %s" + emacs-major-version)) -(setopt initial-major-mode 'fundamental-mode - inhibit-startup-screen t - initial-scratch-message nil - display-time-default-load-average nil - sentence-end-double-space nil - use-short-answers t) + (setopt initial-major-mode 'fundamental-mode + inhibit-startup-screen t + initial-scratch-message nil + display-time-default-load-average nil + sentence-end-double-space nil + use-short-answers t) -(setopt auto-revert-avoid-polling t - auto-revert-interval 5 - auto-revert-check-vc-info t) -(global-auto-revert-mode 1) + (setopt auto-revert-avoid-polling t + auto-revert-interval 5 + auto-revert-check-vc-info t) + (global-auto-revert-mode 1) -(savehist-mode 1) -(recentf-mode 1) + (savehist-mode 1) + (recentf-mode 1) -(windmove-default-keybindings 'control) + (windmove-default-keybindings 'control) -(when (display-graphic-p) - (context-menu-mode -1) - (tool-bar-mode -1) - (menu-bar-mode -1) - (scroll-bar-mode -1)) + (when (display-graphic-p) + (context-menu-mode -1) + (tool-bar-mode -1) + (menu-bar-mode -1) + (scroll-bar-mode -1)) -;; Use Go Mono everywhere. `default' is set directly here; `fixed-pitch' -;; and `variable-pitch' must be pointed at it explicitly so that e.g. -;; `variable-pitch-mode' in org-mode still renders in Go Mono. -(set-face-attribute 'default nil :family "Go Mono" :height 140) -(set-face-attribute 'fixed-pitch nil :family "Go Mono" :height 1.0) -(set-face-attribute 'variable-pitch nil :family "Go Mono" :height 1.0) + (unless (display-graphic-p) + (xterm-mouse-mode 1)) -(unless (display-graphic-p) - (xterm-mouse-mode 1)) + (cua-mode 1) -(cua-mode 1) + (keymap-set global-map "C-x k" #'kill-current-buffer) + (add-hook 'before-save-hook #'whitespace-cleanup) -(keymap-set global-map "C-x k" #'kill-current-buffer) -(add-hook 'before-save-hook #'whitespace-cleanup) + (setopt display-line-numbers-width 3 + tab-bar-show 1) + (add-hook 'prog-mode-hook #'display-line-numbers-mode) + (add-hook 'text-mode-hook #'visual-line-mode) + (dolist (hook '(text-mode-hook prog-mode-hook)) + (add-hook hook #'hl-line-mode)) -(setopt display-line-numbers-width 3 - tab-bar-show 1) -(add-hook 'prog-mode-hook #'display-line-numbers-mode) -(add-hook 'text-mode-hook #'visual-line-mode) -(dolist (hook '(text-mode-hook prog-mode-hook)) - (add-hook hook #'hl-line-mode)) + (require 'server) + (unless (server-running-p) + (server-start)) + (add-hook 'kill-emacs-hook #'server-force-delete) -(require 'server) -(unless (server-running-p) - (server-start)) -(add-hook 'kill-emacs-hook #'server-force-delete) - (defun bedrock--backup-file-name (fpath) "Return a backup path for FPATH, creating parent dirs if needed." - (let* ((backup-root-dir (concat user-emacs-directory "emacs-backup/")) + (let* + ((backup-root-dir + (concat user-emacs-directory "emacs-backup/")) (file-path (replace-regexp-in-string "[A-Za-z]:" "" fpath)) (backup-file-path (replace-regexp-in-string "//" "/" @@ -63,53 +59,135 @@ backup-file-path)) (setopt make-backup-file-name-function #'bedrock--backup-file-name) -(let ((auto-save-dir (expand-file-name "auto-saves/" "~/.cache/emacs")) +(let + ((auto-save-dir + (expand-file-name "auto-saves/" "~/.cache/emacs")) (lock-dir (expand-file-name "locks/" "~/.cache/emacs"))) (make-directory auto-save-dir t) (make-directory lock-dir t) (setopt auto-save-file-name-transforms `((".*" ,auto-save-dir t)) lock-file-name-transforms `((".*" ,lock-dir t)))) +(use-package fontaine + :straight + t + :demand t + :bind ("C-c F" . fontaine-set-preset) + :init + (setq fontaine-latest-state-file + (locate-user-emacs-file "fontaine-latest-state.eld")) + :config + (setq fontaine-presets + '((comic-code + :default-family "Comic Code" + :default-height 120) + (comic-code-large + :default-family "Comic Code" + :default-height 170) + (codelia + :default-family "Codelia" + :default-height 130) + (droid-sans-mono + :default-family "Droid Sans Mono Dotted" + :default-height 130) + (fragment-mono + :default-family "Fragment Mono" + :default-height 130) + (fantasque-sans-mono + :default-family "Fantasque Sans Mono" + :default-height 140) + (go-mono + :default-family "Go Mono" + :default-height 120) + (inconsolata + :default-family "Inconsolata" + :default-weight medium + :default-height 120) + (input-mono + :default-family "Input Mono" + :default-height 120) + (ioskeley + :default-family "Ioskeley Mono Term" + :default-height 120) + (pragmatapro + :default-family "PragmataPro" + :default-height 130) + (0xproto + :default-family "0xProto" + :default-height 120) + (sudo + :default-family "Sudo" + :default-height 140) + (ttyp0 + :default-family "Ttyp0 OTB" + :default-height 130) + (t + :default-weight regular + :default-slant normal + :fixed-pitch-family nil + :variable-pitch-family nil + :bold-weight bold + :italic-slant italic))) + (defun bedrock--fontaine-sync-variable-pitch (&rest _) + (set-face-attribute 'variable-pitch nil + :family (face-attribute 'default :family) + :height 1.0)) + (add-hook 'fontaine-set-preset-hook + #'bedrock--fontaine-sync-variable-pitch) + (fontaine-mode 1) + (fontaine-set-preset + (or (fontaine-restore-latest-preset) 'go-mono))) + (use-package modus-themes - :straight t - :demand t - :config - (load-theme 'modus-operandi t)) + :straight + t + :demand t + :config) -(use-package ef-themes - :straight t - :commands ef-themes-select) + (use-package ef-themes + :straight + t + :demand t + :config) -(use-package which-key - :straight t - :defer 1 - :config (which-key-mode)) + (use-package plan9-theme + :straight + t + :demand t + :config) -(use-package smart-mode-line - :straight t - :demand t - :custom - (sml/no-confirm-load-theme t) - (sml/theme 'respectful) - :config - (sml/setup)) + (use-package which-key + :straight + t + :defer 1 + :config (which-key-mode)) -(use-package minions - :straight t - :demand t - :config - (minions-mode)) + (use-package minions + :straight + t + :demand t + :config + (minions-mode)) + (use-package mood-line + :straight + t + :demand t + :config + (mood-line-mode)) + + ;; Lets `:diminish' hide minor-mode lighters from the mode line. (use-package diminish - :straight t + :straight + t :demand t) (add-to-list 'tab-bar-format 'tab-bar-format-align-right 'append) (add-to-list 'tab-bar-format 'tab-bar-format-global 'append) -(setopt display-time-format "%a %F %T" +(setopt display-time-format "%T" display-time-interval 1) -(display-time-mode 1) +(display-time-mode 0) (setopt mouse-wheel-tilt-scroll t mouse-wheel-flip-direction t @@ -126,31 +204,37 @@ (pixel-scroll-precision-mode 1) (use-package lin - :straight t + :straight + t :hook ((eshell-mode . lin-mode) (dired-mode . lin-mode) (org-mode . lin-mode))) (use-package pulsar - :straight t + :straight + t :config (pulsar-global-mode)) (use-package diff-hl - :straight t + :straight + t :hook ((prog-mode . diff-hl-mode) (org-mode . diff-hl-mode)) :config (global-diff-hl-show-hunk-mouse-mode)) (use-package circadian - :straight (circadian :type git :host github :repo "guidoschmidt/circadian.el") + :straight + (circadian :type git :host github :repo + "guidoschmidt/circadian.el") :demand t :config (setopt calendar-latitude 48.1386 calendar-longitude 17.1053 - circadian-themes '((:sunrise . ef-orange) - (:sunset . modus-vivendi))) + circadian-themes '((:sunrise . plan9) + (:sunset . plan9))) + ;; (:sunset . ef-tritanopia-dark))) (circadian-setup)) (setopt completion-cycle-threshold 3 @@ -159,22 +243,26 @@ (keymap-set minibuffer-mode-map "TAB" #'minibuffer-complete) (use-package vertico - :straight t + :straight + t :init (vertico-mode)) (with-eval-after-load 'vertico (keymap-set vertico-map "M-DEL" #'vertico-directory-delete-word)) (use-package marginalia - :straight t + :straight + t :config (marginalia-mode)) (use-package orderless - :straight t + :straight + t :config (setopt completion-styles '(orderless))) (use-package consult - :straight t + :straight + t :bind (("C-x b" . consult-buffer) ("M-y" . consult-yank-pop) ("M-s r" . consult-ripgrep) @@ -190,16 +278,19 @@ (consult-narrow-key "<")) (use-package embark - :straight t + :straight + t :after consult :bind (("C-c a" . embark-act))) (use-package embark-consult - :straight t + :straight + t :after (embark consult)) (use-package corfu - :straight t + :straight + t :init (global-corfu-mode) :bind (:map corfu-map ("SPC" . corfu-insert-separator))) @@ -212,29 +303,34 @@ (corfu-popupinfo-mode)) (use-package corfu-terminal - :straight t + :straight + t :if (not (display-graphic-p)) :config (corfu-terminal-mode)) (use-package cape - :straight t + :straight + t :init (add-to-list 'completion-at-point-functions #'cape-dabbrev) (add-to-list 'completion-at-point-functions #'cape-file)) (use-package kind-icon - :straight t + :straight + t :if (display-graphic-p) :after corfu :config (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter)) (use-package wgrep - :straight t + :straight + t :config (setopt wgrep-auto-save-buffer t)) (use-package avy - :straight t + :straight + t :bind (("C-c j" . avy-goto-line) ("s-j" . avy-goto-char-timer)) :config @@ -247,10 +343,12 @@ (select-window (cdr (ring-ref avy-ring 0)))) t) - (setf (alist-get ?. avy-dispatch-alist) #'bedrock-avy-action-embark)) + (setf (alist-get ?. avy-dispatch-alist) + #'bedrock-avy-action-embark)) (use-package evil - :straight t + :straight + t :custom (evil-want-keybinding nil) (evil-respect-visual-line-mode t) @@ -262,22 +360,26 @@ (evil-set-initial-state 'eat-mode 'emacs)) (use-package evil-collection - :straight t + :straight + t :after evil :config (evil-collection-init)) (use-package evil-surround - :straight t + :straight + t :after evil :config (global-evil-surround-mode)) (use-package evil-commentary - :straight t + :straight + t :after evil :config (evil-commentary-mode)) (use-package evil-org - :straight t + :straight + t :after org :hook (org-mode . evil-org-mode) :config @@ -285,7 +387,8 @@ (evil-org-agenda-set-keys)) (use-package undo-fu - :straight t + :straight + t :custom (undo-fu-allow-undo-in-region t) (undo-fu-undo-command 'undo-fu-only-undo) @@ -295,7 +398,8 @@ ("M-/" . undo-fu-only-redo-all))) (use-package undo-fu-session - :straight t + :straight + t :custom (undo-fu-session-incompatible-files '("\\.gpg$")) (undo-fu-session-directory @@ -304,7 +408,8 @@ (undo-fu-session-global-mode)) (use-package vundo - :straight t + :straight + t :bind ("C-c u" . vundo)) (with-eval-after-load 'evil @@ -312,7 +417,8 @@ (keymap-set evil-normal-state-map "C-r" #'undo-fu-only-redo)) (use-package helpful - :straight t + :straight + t :bind (("C-h f" . helpful-callable) ("C-h v" . helpful-variable) ("C-h k" . helpful-key) @@ -320,7 +426,8 @@ ("C-h d" . helpful-at-point))) (use-package eshell - :straight (:type built-in) + :straight + (:type built-in) :init (defun bedrock-setup-eshell () "Bind `C-r' to `consult-history' in `eshell-mode'." @@ -328,7 +435,8 @@ :hook (eshell-mode . bedrock-setup-eshell)) (use-package eat - :straight t + :straight + t :after eshell :custom ;; A universally-installed TERM, so shells eat spawns never look up the @@ -339,7 +447,8 @@ (eat-eshell-visual-command-mode)) (use-package tramp - :straight (:type built-in) + :straight + (:type built-in) :defer t :custom (tramp-default-method "ssh") @@ -349,7 +458,8 @@ (add-to-list 'tramp-remote-path 'tramp-own-remote-path)) (use-package treesit-auto - :straight t + :straight + t :demand t :custom (treesit-auto-install t) @@ -360,43 +470,59 @@ (add-hook 'prog-mode-hook #'electric-pair-mode) (use-package project - :straight (:type built-in) + :straight + (:type built-in) :custom (project-mode-line t)) (use-package magit - :straight t + :straight + t :bind (("C-x g" . magit-status))) (use-package yaml-mode - :straight t + :straight + t :mode "\\.ya?ml\\'") (use-package json-mode - :straight t + :straight + t :mode "\\.json\\'") (use-package nix-mode - :straight t + :straight + t :mode "\\.nix\\'") (use-package go-mode - :straight t + :straight + t :mode "\\.go\\'") (use-package zig-mode - :straight t + :straight + t :mode "\\.zig\\'") (use-package rust-mode - :straight t + :straight + t :mode "\\.rs\\'") (use-package elixir-mode - :straight t + :straight + t :mode ("\\.exs?\\'" "mix\\.lock\\'")) +(use-package markdown-mode + :straight + t + :mode "\\.md\\'" + :hook (markdown-mode . visual-line-mode)) + (use-package eglot - :straight (:type built-in) + :straight + (:type built-in) :custom (eglot-send-changes-idle-time 0.1) (eglot-extend-to-xref t) @@ -408,7 +534,8 @@ process-adaptive-read-buffering nil) (use-package tempel - :straight t + :straight + t :bind (("M-*" . tempel-insert) ("M-+" . tempel-complete) :map tempel-map @@ -418,12 +545,14 @@ :init (defun tempel-setup-capf () "Add `tempel-expand' to `completion-at-point-functions' locally." - (add-hook 'completion-at-point-functions #'tempel-expand -1 'local)) + (add-hook 'completion-at-point-functions #'tempel-expand -1 + 'local)) (add-hook 'prog-mode-hook #'tempel-setup-capf) (add-hook 'text-mode-hook #'tempel-setup-capf)) (use-package slime - :straight t + :straight + t :custom (inferior-lisp-program "sbcl") (slime-lisp-implementations '((sbcl ("sbcl")))) @@ -436,7 +565,8 @@ (setopt denote-directory (expand-file-name "~/.nb/notes")) (use-package denote - :straight t + :straight + t :custom (denote-file-type 'org) (denote-known-keywords '("daily" "comp" "music" "misc")) @@ -445,33 +575,39 @@ (denote-rename-buffer-mode)) (use-package denote-journal - :straight t + :straight + t :after denote :custom (denote-journal-keyword "journal") (denote-journal-file-type 'org)) (use-package denote-org - :straight t + :straight + t :after denote) (setopt org-directory (expand-file-name "~/.nb/notes") org-agenda-files (list org-directory) - org-default-notes-file (expand-file-name "todo.org" org-directory)) + org-default-notes-file + (expand-file-name "todo.org" org-directory)) (setopt org-refile-targets '((org-agenda-files :maxlevel . 3)) org-refile-use-outline-path 'file org-outline-path-complete-in-steps nil) (setopt org-todo-keywords - '((sequence "TODO(t)" "WAITING(w@/!)" "STARTED(s!)" "|" "DONE(d!)" "OBSOLETE(o@)")) + '((sequence "TODO(t)" "WAITING(w@/!)" "STARTED(s!)" "|" + "DONE(d!)" "OBSOLETE(o@)")) org-log-done 'time org-log-into-drawer t) (setopt org-capture-templates - '(("t" "task" entry (file+headline org-default-notes-file "Inbox") + '(("t" "task" entry + (file+headline org-default-notes-file "Inbox") "* TODO %?\n%U\n%i") - ("j" "journal" plain (file denote-journal-path-to-new-or-existing-entry) + ("j" "journal" plain + (file denote-journal-path-to-new-or-existing-entry) "%?\n%U\n%i"))) (setopt org-agenda-start-with-log-mode 'clockcheck @@ -493,12 +629,14 @@ org-startup-with-inline-images t org-hide-emphasis-markers t) -(setopt org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id) +(setopt org-id-link-to-org-use-id + 'create-if-interactive-and-no-custom-id) (setopt org-export-with-smart-quotes t) (use-package org - :straight (:type built-in) + :straight + (:type built-in) :hook ((org-mode . visual-line-mode) (org-mode . variable-pitch-mode)) :bind (:map global-map @@ -516,19 +654,17 @@ org-cycle-separator-lines 2)) (use-package org-appear - :straight t + :straight + t :hook (org-mode . org-appear-mode)) (use-package org-super-agenda - :straight t + :straight + t :after org :config (org-super-agenda-mode)) -(use-package markdown-mode - :straight t - :hook (markdown-mode . visual-line-mode)) - (defvar-keymap file-prefix-map :doc "Prefix map for file commands.") (keymap-set global-map "C-c f" file-prefix-map) @@ -567,7 +703,8 @@ :doc "Prefix map for denote commands.") (keymap-set global-map "C-c n" denote-prefix-map) (keymap-set denote-prefix-map "n" #'denote) -(keymap-set denote-prefix-map "j" #'denote-journal-new-or-existing-entry) +(keymap-set denote-prefix-map "j" + #'denote-journal-new-or-existing-entry) (keymap-set denote-prefix-map "l" #'denote-insert-link) (keymap-set denote-prefix-map "L" #'denote-add-links) (keymap-set denote-prefix-map "o" #'denote-open-or-create) @@ -608,29 +745,39 @@ (with-eval-after-load 'evil (evil-set-leader '(normal visual) (kbd "SPC")) - (evil-define-key '(normal visual) 'global (kbd "fh") #'consult-recent-file)) + (evil-define-key '(normal visual) 'global + (kbd "fh") #'consult-recent-file) + (evil-define-key '(normal visual) 'global + (kbd "ff") #'project-find-file) + (evil-define-key '(normal visual) 'global + (kbd "fs") #'save-buffer)) (setopt select-enable-clipboard t select-enable-primary t save-interprogram-text-before-kill t) (use-package clipetty - :straight t + :straight + t :hook (after-init . clipetty-mode)) (with-eval-after-load 'eglot (add-to-list 'eglot-server-programs - '(purescript-mode . ("pnpx" "purescript-language-server" "--stdio"))) + '(purescript-mode + . ("pnpx" "purescript-language-server" "--stdio"))) (add-to-list 'eglot-server-programs - '((markdown-mode text-mode tex-mode typst-mode org-mode) + '((markdown-mode text-mode tex-mode typst-mode + org-mode) . ("harper-ls" "--stdio")))) (use-package zoom-window - :straight t + :straight + t :bind ("C-x z" . zoom-window-zoom)) (use-package tmr - :straight t + :straight + t :bind (("C-c t c" . tmr) ("C-c t l" . tmr-tabulated-view) ("C-c t k" . tmr-remove) @@ -660,8 +807,11 @@ response beginning and end positions; we ignore both." (defun mine/ollama-api-key () "Return the Ollama Cloud API key stored in auth-source. Expects a line in `auth-sources' for host ollama.com, user apikey." - (or (auth-source-pick-first-password :host "ollama.com" :user "apikey") - (user-error "No Ollama API key in auth-source for host ollama.com"))) + (or + (auth-source-pick-first-password :host "ollama.com" :user + "apikey") + (user-error + "No Ollama API key in auth-source for host ollama.com"))) (defun mine/ollama-api-post (path payload) "POST PAYLOAD (an alist) as JSON to Ollama API PATH, return parsed JSON. @@ -669,15 +819,20 @@ Signals an error on a non-2xx response. Runs synchron (let* ((url-request-method "POST") (url-request-extra-headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " (mine/ollama-api-key))))) - (url-request-data (encode-coding-string (json-encode payload) 'utf-8)) - (buf (url-retrieve-synchronously (concat "https://ollama.com" path) + ("Authorization" + . ,(concat "Bearer " (mine/ollama-api-key))))) + (url-request-data + (encode-coding-string (json-encode payload) 'utf-8)) + (buf (url-retrieve-synchronously + (concat "https://ollama.com" path) 'silent nil 60))) (unless buf (error "Ollama API request to %s timed out" path)) (unwind-protect (with-current-buffer buf (goto-char (point-min)) - (let ((status (and (re-search-forward "^HTTP/[0-9.]+ \\([0-9]+\\)" nil t) + (let ((status (and + (re-search-forward + "^HTTP/[0-9.]+ \\([0-9]+\\)" nil t) (string-to-number (match-string 1))))) (unless (re-search-forward "\n\n" nil t) (error "Malformed response from Ollama API")) @@ -686,13 +841,15 @@ Signals an error on a non-2xx response. Runs synchron (json-key-type 'symbol) (body (json-read))) (when (and status (>= status 400)) - (error "Ollama API %s failed (HTTP %d): %s" path status + (error "Ollama API %s failed (HTTP %d): %s" path + status (or (alist-get 'error body) body))) body))) (kill-buffer buf)))) (use-package gptel - :straight (gptel :type git :host github :repo "karthink/gptel") + :straight + (gptel :type git :host github :repo "karthink/gptel") :demand t :bind (:map gptel-mode-map ("C-c RET" . gptel-send) @@ -705,11 +862,16 @@ Signals an error on a non-2xx response. Runs synchron (gptel-use-header-line t) (gptel-org-ignore-elements '(property-drawer)) (gptel-directives - '((default . "You are a large language model living in Emacs and a helpful assistant. Respond concisely.") - (programming . "You are a careful programmer working from inside Emacs. Provide code, with minimal surrounding prose.") - (reviewing . "You are reviewing code or text in Emacs. Be terse, point out concrete issues, and suggest fixes.") - (literate . "You are helping with a literate configuration in an Org file. Prefer Org markup and explain only what is non-obvious.") - (chat . "You are a conversation partner. Respond concisely."))) + '((default + . "You are a large language model living in Emacs and a helpful assistant. Respond concisely.") + (programming + . "You are a careful programmer working from inside Emacs. Provide code, with minimal surrounding prose.") + (reviewing + . "You are reviewing code or text in Emacs. Be terse, point out concrete issues, and suggest fixes.") + (literate + . "You are helping with a literate configuration in an Org file. Prefer Org markup and explain only what is non-obvious.") + (chat + . "You are a conversation partner. Respond concisely."))) (gptel-use-tools t) (gptel-confirm-tool-calls 'auto) (gptel-include-tool-results 'auto) @@ -744,48 +906,63 @@ Signals an error on a non-2xx response. Runs synchron (gptel-make-tool :name "web_search" :category "web" - :description (concat "Search the web and return a list of results, each with a " + :description (concat + "Search the web and return a list of results, each with a " "title, URL and a content snippet. Use this for current " "events or anything outside your training data, then call " "`web_fetch' on a promising URL to read it in full.") :args (list '(:name "query" :type string :description "The search query.") '(:name "max_results" :type integer :optional t - :description "How many results to return (1-10, default 5).")) + :description + "How many results to return (1-10, default 5).")) :function (lambda (query &optional max_results) (let* ((payload `(("query" . ,query) - ,@(when (and (integerp max_results) (> max_results 0)) + ,@(when + (and (integerp max_results) + (> max_results 0)) `(("max_results" . ,(min max_results 10)))))) - (results (alist-get 'results (mine/ollama-api-post "/api/web_search" payload)))) + (results + (alist-get 'results + (mine/ollama-api-post "/api/web_search" + payload)))) (if (null results) "No results found." (mapconcat (lambda (r) (format "## %s\n%s\n\n%s" - (alist-get 'title r) (alist-get 'url r) (alist-get 'content r))) + (alist-get 'title r) (alist-get 'url r) + (alist-get 'content r))) results "\n\n---\n\n"))))) (gptel-make-tool :name "web_fetch" :category "web" - :description "Fetch and return the main text content of a web page, given its URL." + :description + "Fetch and return the main text content of a web page, given its URL." :args (list '(:name "url" :type string - :description "The full URL of the page to fetch.")) + :description + "The full URL of the page to fetch.")) :function (lambda (url) - (let* ((resp (mine/ollama-api-post "/api/web_fetch" `(("url" . ,url)))) + (let* + ((resp + (mine/ollama-api-post "/api/web_fetch" `(("url" . ,url)))) (content (alist-get 'content resp))) (if (and (stringp content) (not (string-empty-p content))) - (format "# %s\n\n%s" (or (alist-get 'title resp) url) content) + (format "# %s\n\n%s" (or (alist-get 'title resp) url) + content) "Could not extract any content from that URL.")))) (gptel-make-tool :name "read_url" :category "web" - :description (concat "Fetch a URL directly (no API) and return its rendered text. " + :description (concat + "Fetch a URL directly (no API) and return its rendered text. " "A fallback for `web_fetch'.") - :args (list '(:name "url" :type string :description "The URL to read.")) + :args + (list '(:name "url" :type string :description "The URL to read.")) :function (lambda (url) (with-current-buffer (url-retrieve-synchronously url t t 30) @@ -802,7 +979,8 @@ Signals an error on a non-2xx response. Runs synchron :category "emacs" :description "Return the full contents of a live Emacs buffer." :args (list '(:name "buffer" :type string - :description "The name of the buffer to read.")) + :description + "The name of the buffer to read.")) :function (lambda (buffer) (unless (buffer-live-p (get-buffer buffer)) @@ -815,7 +993,8 @@ Signals an error on a non-2xx response. Runs synchron :category "filesystem" :description "Read and return the contents of a file on disk." :args (list '(:name "filepath" :type string - :description "Path to the file. Supports relative paths and ~.")) + :description + "Path to the file. Supports relative paths and ~.")) :function (lambda (filepath) (with-temp-buffer @@ -827,24 +1006,31 @@ Signals an error on a non-2xx response. Runs synchron :category "filesystem" :description "List the entries in a directory." :args (list '(:name "directory" :type string - :description "The path to the directory to list.")) + :description + "The path to the directory to list.")) :function (lambda (directory) - (mapconcat #'identity (directory-files (expand-file-name directory)) "\n"))) + (mapconcat #'identity + (directory-files (expand-file-name directory)) "\n"))) (gptel-make-tool :name "run_command" :category "command" - :description "Execute a shell command and return its combined output." + :description + "Execute a shell command and return its combined output." :args (list '(:name "command" :type string - :description "The complete shell command to run.") + :description + "The complete shell command to run.") '(:name "working_dir" :type string :optional t - :description "Directory to run in (default: current).")) + :description + "Directory to run in (default: current).")) :function (lambda (command &optional working_dir) (with-temp-message (format "Executing: %s" command) (let ((default-directory - (if (and working_dir (not (string-empty-p working_dir))) + (if + (and working_dir + (not (string-empty-p working_dir))) (expand-file-name working_dir) default-directory))) (shell-command-to-string command)))) @@ -854,7 +1040,8 @@ Signals an error on a non-2xx response. Runs synchron (gptel-make-tool :name "obscura_scrape" :category "web" - :description (concat "Scrape a web page with the Obscura headless browser " + :description (concat + "Scrape a web page with the Obscura headless browser " "(real JS rendering, stealth). Use this instead of " "`web_fetch'/`read_url' for JavaScript-heavy or " "bot-protected pages. Returns the page as markdown by " @@ -863,20 +1050,29 @@ Signals an error on a non-2xx response. Runs synchron :args (list '(:name "url" :type string :description "The URL to scrape.") '(:name "format" :type string :optional t - :description "Output: markdown (default), text, html, or links.") + :description + "Output: markdown (default), text, html, or links.") '(:name "selector" :type string :optional t - :description "Optional CSS selector to wait for before extracting.")) + :description + "Optional CSS selector to wait for before extracting.")) :function (lambda (url &optional format selector) (if-let* ((exe (executable-find "obscura"))) (with-temp-message (format "Obscura scraping %s" url) (with-output-to-string - (apply #'call-process exe nil (list standard-output nil) nil + (apply #'call-process exe nil + (list standard-output nil) nil "fetch" url - "--dump" (if (member format '("text" "html" "links" "markdown")) - format "markdown") + "--dump" (if + (member format + '("text" "html" "links" + "markdown")) + format + "markdown") "--wait-until" "networkidle0" - (when (and selector (not (string-empty-p selector))) + (when + (and selector + (not (string-empty-p selector))) (list "--selector" selector))))) "Error: the `obscura' binary is not on PATH. Install it (https://github.com/h4ckf0r0day/obscura) or use `web_fetch'/`read_url' instead.")) :confirm t @@ -891,8 +1087,10 @@ Signals an error on a non-2xx response. Runs synchron ;; Presets: apply with `C-c L p' or an `@name' cookie at the start of a prompt. (gptel-make-preset 'research - :description "Web research: search, fetch and scrape, then synthesise." - :system (concat "You are a research assistant working inside Emacs. Use the " + :description + "Web research: search, fetch and scrape, then synthesise." + :system (concat + "You are a research assistant working inside Emacs. Use the " "web tools to gather up-to-date information before answering: " "`web_search' to find sources, `web_fetch'/`read_url' to read " "them, and `obscura_scrape' for JavaScript-heavy or protected " @@ -901,19 +1099,23 @@ Signals an error on a non-2xx response. Runs synchron :confirm-tool-calls nil) (gptel-make-preset 'agent - :description "Coding agent: read files/buffers and run commands in Emacs." - :system (concat "You are a careful coding agent operating inside Emacs. Use " + :description + "Coding agent: read files/buffers and run commands in Emacs." + :system (concat + "You are a careful coding agent operating inside Emacs. Use " "the filesystem and buffer tools to inspect the project, and " "`run_command' to build, test or inspect the repo. Explain " "what you will do, then act. Prefer minimal, surgical changes.") - :tools '("read_file" "read_buffer" "list_directory" "run_command") + :tools + '("read_file" "read_buffer" "list_directory" "run_command") :confirm-tool-calls t) (gptel-make-preset 'full :description "Everything: web + filesystem + Emacs + commands." :system 'default :tools '("web_search" "web_fetch" "read_url" "obscura_scrape" - "read_file" "read_buffer" "list_directory" "run_command")) + "read_file" "read_buffer" "list_directory" + "run_command")) (gptel-make-preset 'chat :description "Plain conversation, no tools." @@ -931,79 +1133,99 @@ Signals an error on a non-2xx response. Runs synchron (minimax . minimax-m3:cloud) (minimax2 . minimax-m2.7:cloud))) (gptel-make-preset (car spec) - :description (format "Use the %s model on Ollama Cloud." (cdr spec)) + :description + (format "Use the %s model on Ollama Cloud." (cdr spec)) :backend "Ollama Cloud" :model (cdr spec)))) ;; Bundled with gptel (same repo, already on `load-path'); no recipe needed. (use-package gptel-transient - :after gptel + :after + gptel :commands (gptel-menu gptel-system-prompt gptel-tools)) (use-package dockerfile-mode - :straight t + :straight + t :mode (("\\.Dockerfile\\'" . dockerfile-mode) ("/Dockerfile\\'" . dockerfile-mode))) (use-package docker - :straight t + :straight + t :commands (docker docker-compose)) (use-package nginx-mode - :straight t + :straight + t :mode "\\.nginx\\'") (use-package toml-mode - :straight t + :straight + t :mode "\\.toml\\'") (use-package ini-mode - :straight t + :straight + t :mode (("\\.ini\\'" . ini-mode) - ("/\\.env\\.\\(?:local\\|development\\|production\\)?\\'" . ini-mode))) + ("/\\.env\\.\\(?:local\\|development\\|production\\)?\\'" + . ini-mode))) (use-package restclient - :straight t + :straight + t :mode (("\\.http\\'" . restclient-mode) ("\\.rest\\'" . restclient-mode))) (use-package graphql-mode - :straight t + :straight + t :mode "\\.gql\\(?:s\\)?\\'") (use-package docker-compose-mode - :straight t - :mode (("docker-compose\\(?:\\..*\\)?\\.ya?ml\\'" . docker-compose-mode) - ("compose\\.ya?ml\\'" . docker-compose-mode))) + :straight + t + :mode + (("docker-compose\\(?:\\..*\\)?\\.ya?ml\\'" . docker-compose-mode) + ("compose\\.ya?ml\\'" + . docker-compose-mode))) (use-package ob-async - :straight t + :straight + t :after org) (use-package ob-restclient - :straight t + :straight + t :after org) (use-package ob-http - :straight t + :straight + t :after org) (use-package ob-graphql - :straight t + :straight + t :after org) (use-package ob-mermaid - :straight t + :straight + t :after org :config (setopt ob-mermaid-cli-path (or (executable-find "mmdc") "npx"))) (use-package ob-d2 - :straight t + :straight + t :after org) (use-package ob-llm - :straight t + :straight + t :after org :config ;; Send babel blocks through gptel rather than a hosted API directly. @@ -1011,7 +1233,8 @@ Signals an error on a non-2xx response. Runs synchron ob-llm-stream t)) (use-package ob-tmux - :straight t + :straight + t :after org :config (setopt ob-tmux-location-alist @@ -1020,9 +1243,14 @@ Signals an error on a non-2xx response. Runs synchron (with-eval-after-load 'org (setopt org-babel-default-languages - '(emacs-lisp shell python docker dockerfile restclient http graphql mermaid d2 tmux llm)) + '(emacs-lisp shell python docker dockerfile restclient + http graphql mermaid d2 tmux llm)) (setopt org-confirm-babel-evaluate nil - org-babel-default-header-args:shell '((:results . "output") (:async . "yes")) - org-babel-default-header-args:tmux '((:results . "output") (:async . "yes")) - org-babel-default-header-args:restclient '((:results . "raw")) - org-babel-default-header-args:http '((:results . "raw")))) + org-babel-default-header-args:shell + '((:results . "output") (:async . "yes")) + org-babel-default-header-args:tmux + '((:results . "output") (:async . "yes")) + org-babel-default-header-args:restclient + '((:results . "raw")) + org-babel-default-header-args:http + '((:results . "raw"))))