diff --git a/flake.lock b/flake.lock index 96d94f5..7defb6b 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1789074092, - "narHash": "sha256-QCXTUbd4FuwOHO7LgJws78YTRcco4xgR8RoYgKLvd9w=", + "lastModified": 1790280108, + "narHash": "sha256-bApGLOF5XDhf3UCM1/e1yIQCoY5wW1+B88lHkcYlp2U=", "owner": "sadjow", "repo": "claude-code-nix", - "rev": "1f88b393ac054378c393f914dc83d08a6ef5daeb", + "rev": "f10421316cc6df045497f5aa0009adce648e89ee", "type": "github" }, "original": { @@ -28,11 +28,11 @@ ] }, "locked": { - "lastModified": 1789267039, - "narHash": "sha256-LWiBv9yAYFi2LPbUhDGHPGKYskJQjj2fw12OlyO1uQo=", + "lastModified": 1790215976, + "narHash": "sha256-VkR1rMGyjU9jtw8ISFx5uqR9e+LEaOlhHwrseFkiIVk=", "owner": "nix-community", "repo": "home-manager", - "rev": "ec172013fa62135f58fb58dd17ae9651e8f39727", + "rev": "a6631107a83ceab5872f298a2ea710859c80c4cb", "type": "github" }, "original": { @@ -49,11 +49,11 @@ ] }, "locked": { - "lastModified": 1788680125, - "narHash": "sha256-amGSoDobwmp4CFvCn841ws2iuitus+HUJdD/gKgsrJA=", + "lastModified": 1789892076, + "narHash": "sha256-Dkg4VKPmDPqTwaiw2WH5br73tXoL1qrOO0xJnR4TlcA=", "owner": "nix-community", "repo": "nix-index-database", - "rev": "116ad1c2adb642405ef8916f6a94c8626f971344", + "rev": "9ad722673ab3b3f91f02135e53775825b240b869", "type": "github" }, "original": { @@ -64,11 +64,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1789009968, - "narHash": "sha256-GB16oaxpsrnGNnGIE+0uYBqMRzB9X6FYDUvjvnJAYew=", + "lastModified": 1790218706, + "narHash": "sha256-6e4Na3z008XpdVyOXgfasXIn1aN+z8AXFG+jXdQSyuI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d58a46e3bc02d91ebe04667f8397752a749c0024", + "rev": "c508844df6c28fa6dabc1b6af70f3ccbd65c5201", "type": "github" }, "original": { diff --git a/home.org b/home.org index 7ff0782..6522390 100644 --- a/home.org +++ b/home.org @@ -866,13 +866,17 @@ Actually, nix *really* requires ~bash~. This is a plugin that allows using zsh f :custom (claude-code-optimize-window-resize nil)) (use-package gptel + :commands "gptel" + :bind (("C-c g" . gptel-menu)) :custom (require 'org) (gptel-default-mode 'org-mode) (gptel-model 'claude-sonnet-5)) (use-package gptel-agent + :after gptel :config + (require 'gptel-agent-tools-introspection) (gptel-agent-update) (gptel-make-preset 'coding :description "Preset for agentic coding" @@ -880,13 +884,26 @@ Actually, nix *really* requires ~bash~. This is a plugin that allows using zsh f :model 'claude-sonnet-5 :use-tools t :tools '("Agent" "Skill" "TodoWrite" "Grep" "Read" "Glob" "Write" "Insert" - "Edit" "Mkdir" "Diagnostics" "WebFetch" "WebSearch" "Eval" "Bash")) + "Edit" "Mkdir" "Diagnostics" "WebFetch" "WebSearch" "Bash")) (gptel-make-preset 'research - :description "Preset for agentic coding" + :description "Preset for answering questions with web results" :backend "Claude" :model 'claude-sonnet-5 :use-tools t - :tools '("WebFetch" "WebSearch"))) + :confirm-tool-calls nil + :tools '("WebFetch" "WebSearch")) + (gptel-make-preset 'emacs-research + :description "Preset for answering questions about the live emacs instance" + :backend "Claude" + :model 'claude-sonnet-5 + :use-tools t + :confirm-tool-calls nil + :tools '("WebFetch" "WebSearch" + "variable_completions" "command_completions" "function_completions" + "variable_documentation" "function_documentation" "variable_value" + "variable_source" "function_source" "symbol_manual_section" + "library_source" "manual_node_contents" "manual_nodes" "manual_names" + "features" "load_paths" "symbol_exists"))) #+END_SRC ** Age @@ -1004,7 +1021,6 @@ Turn off some default settings that I don't find useful. (setq-default truncate-lines t) (setopt tab-bar-show nil) - (tab-bar-mode) #+END_SRC Don't show the welcome screen, and keep the scratch buffer empty by default. These are not useful @@ -1203,20 +1219,26 @@ A basic ace-window setup. Allows switching between windows quickly (although avy Set up a few project- / workspace-related key bindings. -#+BEGIN_SRC elisp - (bind-keys ("s-f" . project-find-file) - ("s-F" . project-switch-project) - ;; ("s-s" . persp-switch) - ) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage = { + project = { + enable = true; + bind = { + "s-f" = "project-find-file"; + "s-F" = "project-switch-project"; + }; + }; - (with-eval-after-load 'tab-bar - (bind-keys :map tab-bar-mode-map - ("s-s" . tab-bar-switch-to-tab))) + tab-bar = { + enable = true; + hook = ["after-init"]; + bindLocal.tab-bar-mode-map."s-s" = "tab-bar-switch-to-tab"; + }; + }; + } #+END_SRC -#+RESULTS: -: tab-bar-switch-to-tab - Don't run a server. I'm not using this, I can always turn it back on if I need to. #+BEGIN_SRC elisp @@ -1258,22 +1280,17 @@ Make compilation mode buffers auto-scroll to the first error: (setopt compilation-scroll-output 'first-error) #+END_SRC -#+begin_src elisp - (use-package isearch - :bind (:map isearch-mode-map - ("M-e" . consult-isearch-history) - ("M-s e" . consult-isearch-history) - ("M-s l" . consult-line) - ("M-s L" . consult-line-multi))) -#+end_src - -#+begin_src elisp - (use-package register - :defer t - :custom - (register-preview-delay 0.5) - :config - (advice-add 'register-preview :override #'consult-register-window)) +#+begin_src nix + { + programs.emacs.init.usePackage.register = { + enable = true; + defer = true; + custom.register-preview-delay = 0.5; + config = '' + (advice-add 'register-preview :override #'consult-register-window) + ''; + }; + } #+end_src *** Window Placement @@ -1282,7 +1299,79 @@ going for an IDE-style layout, with one main editor window, and auxiliary bottom windows. I place windows in the bottom or on the right mostly depending on how wide I expect their content to be. -#+BEGIN_SRC elisp +#+begin_src nix + { + programs.emacs.init.usePackage = { + window = { + enable = true; + custom = { + split-width-threshold = 200; + split-height-threshold = false; + display-buffer-base-action = '''((display-buffer-reuse-window + display-buffer-reuse-mode-window + display-buffer-use-some-window))''; + window-persistent-parameters = '''(cl-list* '(no-delete-other-windows . writable) + '(no-other-window . writable) + window-persistent-parameters)''; + }; + init = '' + <> + ''; + config = '' + <> + ''; + }; + + popper = { + enable = true; + hook = ["after-init" "(after-init . popper-echo-mode)"]; + bind = { + "C-q" = "popper-toggle"; + "C-;" = "popper-cycle"; + }; + custom = { + popper-display-control = false; + popper-reference-buffers = ''(concat my/bottom-popups my/right-popups)''; + }; + }; + }; + } +#+end_src + +#+begin_src elisp :tangle no :noweb-ref emacs-window-init + (defvar jf/bottom-popups + '("\\*Flymake diagnostics .*" + "\\*Embark Export: .*" + "\\*Embark Collect .*" + "\\*xref\\*" + "\\*compilation\\*" + "\\*pnpm:.*" + "\\*sly-mrepl *" + "\\*lispy-message\\*" + "\\*occur\\*" + ".*eshell\\*" + "\\*SQL: .*")) + + (defvar jf/right-popups + '("magit: *" + "magit-log*" + "\\*Messages\\*" + "\\*trace-output\\*" + "\\*EGLOT connections\\*" + "magit-revision: .*" + "\\*Help\\*" + "\\*rustfmt\\*" + "\\*slack: .*" + "\\*forge: .*" + "new-pullreq" + "\\*claude:.*" + "\\*Man .*" + "\\*List .*" + "\\*Embark Actions\\*" + "\\*eldoc\\*")) +#+end_src + +#+BEGIN_SRC elisp :tangle no :noweb-ref emacs-window-init (defun jf/window-children (window) (let ((result nil) (child (window-child window))) @@ -1343,188 +1432,48 @@ content to be. (window--display-buffer buffer right 'reuse alist))) (t (window--display-buffer buffer (jf/topright-window (jf/window-leaves main)) 'reuse alist))))) - - - (setopt popper-reference-buffers - '("\\*Flymake diagnostics .*" - "\\*Embark Export: .*" - "\\*Embark Collect .*" - "\\*xref\\*" - "\\*compilation\\*" - "\\*pnpm:.*" - "\\*sly-mrepl *" - "\\*lispy-message\\*" - "\\*occur\\*" - ".*eshell\\*" - "\\*SQL: .*" - "magit-revision: .*" - "\\*Help\\*" - "\\*rustfmt\\*" - "\\*slack: .*" - "\\*forge: .*" - "new-pullreq" - "\\*claude:.*" - "\\*Man .*" - "\\*List .*" - "\\*eldoc\\*")) - (setopt popper-display-control nil) - - (popper-mode) - (popper-echo-mode) - - (bind-keys ("C-q" . popper-toggle) - ("C-;" . popper-cycle)) - - (defvar jf/fullscreen-popup--param 'jf/fullscreen-saved-configuration - "Key used to stash the window configuration a fullscreen popup replaced. - Stored per-tab when `tab-bar-mode' is active, otherwise as a plain frame - parameter; see `jf/tab-or-frame-parameter'.") - - (defvar-local jf/fullscreen-popup-p nil - "Non-nil in a buffer currently being displayed as a fullscreen popup.") - - (defun jf/current-tab-cell (&optional frame) - "Return FRAME's current tab's raw data cell, or nil without `tab-bar-mode'." - (and (bound-and-true-p tab-bar-mode) - (assq 'current-tab (tab-bar-tabs frame)))) - - (defun jf/tab-or-frame-parameter (parameter &optional frame) - "Like `frame-parameter', but scoped to FRAME's selected tab when tabs are in use. - - A plain frame parameter is shared by every tab on that frame, which is wrong - for state like `jf/fullscreen-popup--param': switching tabs swaps in an - entirely different window layout, so \"the layout to restore\" has to travel - with the tab, not the frame." - (if-let ((tab (jf/current-tab-cell frame))) - (alist-get parameter tab) - (frame-parameter frame parameter))) - - (defun jf/set-tab-or-frame-parameter (parameter value &optional frame) - "Set PARAMETER to VALUE, scoped like `jf/tab-or-frame-parameter'." - (if-let ((tab (jf/current-tab-cell frame))) - (let ((cell (assq parameter tab))) - (if cell - (setcdr cell value) - (setcdr tab (cons (cons parameter value) (cdr tab))))) - (set-frame-parameter frame parameter value))) - - (defun jf/display-buffer-fullscreen (buffer alist) - "Display BUFFER, deleting other windows, saving the prior layout. - - Intended for use in `display-buffer-alist' for popups (e.g. Magit - status/log) that should take over the frame while displayed, but - restore the previous window configuration once closed via either - `q' or `popper-toggle' -- both end up calling `quit-window', which - is where the restore actually happens; see - `jf/quit-window-restore-fullscreen'." - (unless (jf/tab-or-frame-parameter jf/fullscreen-popup--param) - (jf/set-tab-or-frame-parameter jf/fullscreen-popup--param - (current-window-configuration))) - (when-let ((window (or (display-buffer-reuse-window buffer alist) - (display-buffer-same-window buffer alist) - (display-buffer-pop-up-window buffer alist) - (display-buffer-use-some-window buffer alist)))) - (let ((ignore-window-parameters t)) - (delete-other-windows window)) - (with-current-buffer buffer - (setq jf/fullscreen-popup-p t)) - window)) - - (defun jf/quit-window-restore-fullscreen (orig-fn &optional kill window) - "Restore the saved window configuration after quitting a fullscreen popup. - - Advice around `quit-window' so both `q' (which Magit and other - special-mode buffers bind to buffer-quitting commands that call - `quit-window') and Popper's close/toggle commands (which also call - `quit-window' directly) trigger the restore. Only restores once the - last fullscreen popup on the tab (or frame, without `tab-bar-mode') - has closed, so opening one fullscreen popup from within another - falls back to the outer one first." - (let* ((window (or window (selected-window))) - (was-fullscreen (buffer-local-value 'jf/fullscreen-popup-p - (window-buffer window))) - (frame (window-frame window))) - (funcall orig-fn kill window) - (when (and was-fullscreen - (not (and (window-live-p window) - (buffer-local-value 'jf/fullscreen-popup-p - (window-buffer window)))) - (jf/tab-or-frame-parameter jf/fullscreen-popup--param frame)) - (let ((conf (jf/tab-or-frame-parameter jf/fullscreen-popup--param frame))) - (jf/set-tab-or-frame-parameter jf/fullscreen-popup--param nil frame) - (set-window-configuration conf))))) - - (advice-add 'quit-window :around #'jf/quit-window-restore-fullscreen) - - (setopt split-width-threshold 200) - (setopt split-height-threshold nil) - (setopt display-buffer-base-action - '((display-buffer-reuse-window - display-buffer-reuse-mode-window - display-buffer-use-some-window))) - - (setq display-buffer-alist - '(((or "\\*Flymake diagnostics .*" - "\\*Embark Export: .*" - "\\*Embark Collect .*" - "\\*xref\\*" - "\\*compilation\\*" - "\\*pnpm:.*" - "\\*sly-mrepl *" - "\\*lispy-message\\*" - "\\*occur\\*" - ".*eshell\\*" - "\\*SQL: .*") - (display-buffer-in-side-window) - (side . bottom) - (slot . 0) - (window-height . 0.33)) - ((or "magit: *" - "magit-log*" - "\\*Messages\\*" - "\\*trace-output\\*" - "\\*EGLOT connections\\*" - "magit-revision: .*" - "\\*Help\\*" - "\\*rustfmt\\*" - "\\*slack: .*" - "\\*forge: .*" - "new-pullreq" - "\\*claude:.*" - "\\*Man .*" - "\\*List .*" - "\\*Embark Actions\\*" - "\\*eldoc\\*") - (display-buffer-reuse-window - display-buffer-reuse-mode-window - jf/display-buffer-right)))) - - (setopt window-persistent-parameters - (cl-list* '(no-delete-other-windows . writable) - '(no-other-window . writable) - window-persistent-parameters)) #+END_SRC -** Editing -This configuration affects general text editing, across all modes. First, some packages: +#+begin_src elisp :tangle no :noweb-ref emacs-window-config + (setq display-buffer-alist + `(((or ,@jf/bottom-popups) + (display-buffer-in-side-window) + (side . bottom) + (slot . 0) + (window-height . 0.33)) + ((or ,@jf/right-popups) + (display-buffer-reuse-window + display-buffer-reuse-mode-window + jf/display-buffer-right)))) +#+end_src -#+BEGIN_SRC nix +*** Tramp +#+begin_src nix { - programs.emacs = { - extraPackages = epkgs: with epkgs; [ - apheleia - avy - edit-indirect - embark - god-mode - # evil - # evil-collection - # evil-org - string-inflection - ]; + programs.emacs.init.usePackage.tramp = { + enable = true; + package = (epkgs: epkgs.tramp.overrideAttrs { + version = "2.8.2.3"; + src = pkgs.fetchurl { + url = "https://elpa.gnu.org/packages/tramp-2.8.2.3.tar"; + sha256 = "sha256-8Rsn29+i7LDGDKFR1Q7HTml86MRmibN409D9QaGBci0="; + }; + }); + config = '' + <> + ''; }; } -#+END_SRC +#+end_src + +#+begin_src elisp :noweb no :noweb-ref emacs-tramp-config + (with-eval-after-load 'compile + (remove-hook 'compilation-mode-hook + #'tramp-compile-disable-ssh-controlmaster-options)) +#+end_src + +** Editing +This configuration affects general text editing, across all modes. I find Emacs to be almost unusable on a normal keyboard unless there's some way to avoid all the modifier keys. I tried Evil mode first. But although I love Vim, it just seemed to be hiding too @@ -1537,98 +1486,99 @@ but I'm worried that having too many repeat maps will also be annoying (I'll hav cancel them more frequently). In the end, I opted for Devil mode. It avoids modifier keys, and has its own devil-mode-only repeat maps that don't interfere when using normal modifier key sequences. - #+BEGIN_SRC elisp - ;; (setopt evil-want-keybinding nil) - ;; (require 'evil) - ;; (evil-mode 1) - ;; (require 'evil-collection) - ;; (evil-collection-init) - - ;; (require 'evil-org) - ;; (add-hook 'org-mode-hook 'evil-org-mode) - ;; (evil-org-set-key-theme '(navigation insert textobjects additional calendar)) - ;; (require 'evil-org-agenda) - ;; (evil-org-agenda-set-keys) - (require 'god-mode) - (require 'which-key) - - (bind-keys :map god-local-mode-map - ("i" . (lambda () (interactive) (god-mode-all -1))) - ("z" . repeat)) - (bind-keys ("" . (lambda () (interactive) (god-mode-all 1)))) - (defun my-god-mode-update-cursor-type () - (setq cursor-type (cond - ((and (featurep 'org-present) org-present-mode) nil) - ((or (and (featurep 'god-mode) god-local-mode) buffer-read-only) 'box) - (t 'bar)))) - - (add-hook 'post-command-hook 'my-god-mode-update-cursor-type) - (which-key-enable-god-mode-support) + #+BEGIN_SRC nix + { + programs.emacs.init.usePackage.god-mode = { + enable = true; + bind."" = "(lambda () (interactive) (god-mode-all 1))"; + bindLocal.god-local-mode-map = { + i = "(lambda () (interactive) (god-mode-all -1))"; + z = "repeat"; + }; + init = '' + (defun my-god-mode-update-cursor-type () + (setq cursor-type (cond + ((and (featurep 'org-present) org-present-mode) nil) + ((or (and (featurep 'god-mode) god-local-mode) buffer-read-only) 'box) + (t 'bar)))) + + (add-hook 'post-command-hook 'my-god-mode-update-cursor-type) + ''; + config = '' + (with-eval-after-load 'which-key + (which-key-enable-god-mode-support)) + ''; + }; + } #+END_SRC Avy allows jumping to any place on the screen with just a few keystrokes. I've stolen most of these ideas from Karthinks' article [[https://karthinks.com/software/avy-can-do-anything/][Avy can do Anything]]. I need to find more uses for this, in line with his article. -#+BEGIN_SRC elisp - (defun avy-goto-char-timer-embark () - (interactive) - (require 'avy) - (let ((avy-action-oneshot - (lambda (pt) - (goto-char pt) - (embark-act)))) - (avy-goto-char-timer))) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage = { + avy = { + enable = true; + functions = ["avy-action-oneshot"]; + init = '' + (defun avy-goto-char-timer-embark () + (interactive) + (let ((avy-action-oneshot + (lambda (pt) + (goto-char pt) + (embark-act)))) + (avy-goto-char-timer))) + ''; + bind = { + "C-j" = "avy-goto-char-timer"; + "M-j" = "avy-goto-char-timer-embark"; + }; + }; - (bind-keys - ("C-j" . avy-goto-char-timer) - ("M-j" . avy-goto-char-timer-embark) - ("C-." . embark-act)) + embark = { + enable = true; + bind."C-." = "embark-act"; + }; - (with-eval-after-load 'lispy - (bind-keys :map lispy-mode-map - ("C-j" . avy-goto-char-timer))) - - (with-eval-after-load 'org - (bind-keys :map org-mode-map - ("C-j" . avy-goto-char-timer))) + lispy.bindLocal.lispy-mode-map."C-j" = "avy-goto-char-timer"; + org.bindLocal.org-mode-map."C-j" = "avy-goto-char-timer"; + }; + } #+END_SRC Just "silently" wrap when searching at the bottom of a document. -#+BEGIN_SRC elisp - (setopt isearch-wrap-pause 'no-ding) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.isearch = { + enable = true; + custom.isearch-wrap-pause = "'no-ding"; + }; + } #+END_SRC -#+BEGIN_SRC elisp - (defun string-inflection-rust-style-cycle-function (str) - (cond - ((string-inflection-snake-case-p str) - (string-inflection-upcase-function str)) - ((string-inflection-upcase-p str) - (string-inflection-pascal-case-function str)) - (t - (string-inflection-snake-case-function str)))) - - (defun string-inflection-rust-style-cycle () - (interactive) - (require 'string-inflection) - (string-inflection--symbol-or-region #'string-inflection-rust-style-cycle-function)) - - (bind-keys :repeat-map string-inflection-java-map - ("u" . string-inflection-java-style-cycle)) - - (bind-keys :repeat-map string-inflection-rust-map - ("u" . string-inflection-rust-style-cycle)) - - (bind-keys ("C-x 4 n" . edit-indirect-region)) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.edit-indirect = { + enable = true; + bind."C-x 4 n" = "edit-indirect-region"; + }; + } #+END_SRC Some auto-formatting stuff -#+begin_src elisp - (with-eval-after-load 'apheleia - (push '(prisma-format "prisma" "format" "--schema" filepath) apheleia-formatters)) +#+begin_src nix + { + programs.emacs.init.usePackage.apheleia = { + enable = true; + config = '' + (push '(prisma-format "prisma" "format" "--schema" filepath) apheleia-formatters) + ''; + }; + } #+end_src ** Workflow @@ -1777,7 +1727,7 @@ available? It seems to work for now anyway... programs.emacs.init.usePackage.ansi-color = { enable = true; defer = true; - init = "(add-hook 'compilation-filter 'ansi-color-compilation-filter)"; + hook = ["(compilation-filter . ansi-color-compilation-filter)"]; }; } #+end_src @@ -1818,7 +1768,7 @@ I don't like UI elements that insert fake text into the file. They mess up the s programs.emacs.init.usePackage.eglot = { custom = { eglot-ignored-server-capabilities = "'(:inlayHintProvider :semanticTokensProvider)"; - eglot-code-actoins-indications = false; + eglot-code-actions-indications = false; eglot-events-buffer-config = "'(:size 300000 :format full)"; }; config = "(add-hook 'eglot-managed-mode-hook (lambda () (eglot-inlay-hints-mode -1)) nil t)"; @@ -1853,7 +1803,6 @@ for completion. That makes it really annoying to use; don't use yasnippet with e programs.emacs.init.usePackage.rust-mode = { enable = true; mode = ["\"\\\\.rs\\\\'\""]; - bindLocal.rust-mode-map."C-c u" = "string-inflection-rust-style-cycle"; custom.rust-mode-treesitter-derive = true; }; } @@ -2028,9 +1977,6 @@ for completion. That makes it really annoying to use; don't use yasnippet with e java-ts-mode = { enable = true; init = ''(push '(java-mode . java-ts-mode) major-mode-remap-alist)''; - bindLocal.java-ts-mode-map = { - "C-c u" = "string-inflection-java-style-cycle"; - }; config = '' <> ''; @@ -2240,6 +2186,16 @@ xs consult-eglot.enable = true; + isearch = { + enable = true; + bindLocal.isearch-mode-map = { + "M-e" = "consult-isearch-history"; + "M-s e" = "consult-isearch-history"; + "M-s l" = "consult-line"; + "M-s L" = "consult-line-multi"; + }; + }; + xref = { enable = true; defer = true;