diff --git a/flake.lock b/flake.lock index 7defb6b..d21f767 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1790280108, - "narHash": "sha256-bApGLOF5XDhf3UCM1/e1yIQCoY5wW1+B88lHkcYlp2U=", + "lastModified": 1791071338, + "narHash": "sha256-YBAFoulgm9qEavWtmb2gHwk7lgpVZL7vtr4+yi6H3UU=", "owner": "sadjow", "repo": "claude-code-nix", - "rev": "f10421316cc6df045497f5aa0009adce648e89ee", + "rev": "b17e42820330bd34ab890632c2f6f80daf4e517f", "type": "github" }, "original": { @@ -28,11 +28,11 @@ ] }, "locked": { - "lastModified": 1790215976, - "narHash": "sha256-VkR1rMGyjU9jtw8ISFx5uqR9e+LEaOlhHwrseFkiIVk=", + "lastModified": 1790996401, + "narHash": "sha256-f+kOxEmOUnQazVAmGg0i8hzwgd1kX/geY+k/Dpheye8=", "owner": "nix-community", "repo": "home-manager", - "rev": "a6631107a83ceab5872f298a2ea710859c80c4cb", + "rev": "db7d5e2332710f5abb088f6b5de927d7f9511b35", "type": "github" }, "original": { @@ -49,11 +49,11 @@ ] }, "locked": { - "lastModified": 1789892076, - "narHash": "sha256-Dkg4VKPmDPqTwaiw2WH5br73tXoL1qrOO0xJnR4TlcA=", + "lastModified": 1791105314, + "narHash": "sha256-oTM7QFgRQ+vxBpMBMc7hEYpqBtpXAzXu2xuB/tru84U=", "owner": "nix-community", "repo": "nix-index-database", - "rev": "9ad722673ab3b3f91f02135e53775825b240b869", + "rev": "e740bc4e9d8918d7819a51486d5099930c65c72e", "type": "github" }, "original": { @@ -64,11 +64,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1790218706, - "narHash": "sha256-6e4Na3z008XpdVyOXgfasXIn1aN+z8AXFG+jXdQSyuI=", + "lastModified": 1791143449, + "narHash": "sha256-9+n3ReS2y9xS199LoJjKUWHwZrISGHWgkD/LUZTDZyA=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c508844df6c28fa6dabc1b6af70f3ccbd65c5201", + "rev": "0d9e9b832d03ac387417e16ce1febf73b2e631e1", "type": "github" }, "original": { @@ -94,11 +94,11 @@ ] }, "locked": { - "lastModified": 1788678358, - "narHash": "sha256-xFu9deElkBd81tRYo2yRSZQ40LDXMnVd/gO1/uELxhI=", + "lastModified": 1790708379, + "narHash": "sha256-UShpDPmypGviorjBWDwRtKWUTdVik7Zp6bt1t875jV4=", "owner": "~rycee", "repo": "nur-expressions", - "rev": "2554c0e25259c6ab3e71dac5177f8d38f76a6e1c", + "rev": "b549e36268fa8c975535d0405f66e013b234a0a4", "type": "sourcehut" }, "original": { diff --git a/home.org b/home.org index 6522390..3ce50dd 100644 --- a/home.org +++ b/home.org @@ -801,46 +801,69 @@ Actually, nix *really* requires ~bash~. This is a plugin that allows using zsh f } #+END_SRC -** Claude Code +** LLM :PROPERTIES: :header-args:elisp: :tangle target/emacs.el :END: #+BEGIN_SRC nix + (let gptel = (epkgs: epkgs.melpaBuild { + pname = "gptel"; + version = "0.9.9.6"; + src = pkgs.fetchFromGitHub { + owner = "karthink"; + repo = "gptel"; + rev = "v0.9.9.6"; + sha256 = "sha256-jurm3AQz+NiKZh7ssJkQYP73EmBEoyjzuC4phARYBak="; + }; + recipe = pkgs.writeText "gptel-recipe" '' + (gptel :repo "karthink/gptel" :fetcher github) + ''; + packageRequires = [ epkgs.transient ]; + }); + in { programs.claude-code = { enable = true; }; + + programs.emacs.init.usePackage = { + claude-code = { + enable = true; + package = (epkgs: epkgs.trivialBuild { + pname = "claude-code"; + version = "0.4.5"; + src = pkgs.fetchFromGitHub { + owner = "stevemolitor"; + repo = "claude-code.el"; + rev = "03199df8b3a1e9cd4857f0851f7a912ba524aff3"; + sha256 = "sha256-5QJrWIu4EgnHcOqMwlrs2JBBx7aI9OaSJswesr6Apfk="; + }; + packageRequires = [ epkgs.inheritenv ]; + }); + bindKeyMap."C-c c" = "claude-code-command-map"; + custom.claude-code-optimize-window-resize = false; + }; - programs.emacs = { - extraPackages = epkgs: with epkgs; - let gptel = (epkgs.melpaBuild { - pname = "gptel"; - version = "0.9.9.6"; - src = pkgs.fetchFromGitHub { - owner = "karthink"; - repo = "gptel"; - rev = "v0.9.9.6"; - sha256 = "sha256-jurm3AQz+NiKZh7ssJkQYP73EmBEoyjzuC4phARYBak="; - }; - recipe = pkgs.writeText "gptel-recipe" '' - (gptel :repo "karthink/gptel" :fetcher github) - ''; - packageRequires = [ epkgs.transient ]; - }); - in [ - (epkgs.trivialBuild { - pname = "claude-code"; - version = "0.4.5"; - src = pkgs.fetchFromGitHub { - owner = "stevemolitor"; - repo = "claude-code.el"; - rev = "03199df8b3a1e9cd4857f0851f7a912ba524aff3"; - sha256 = "sha256-5QJrWIu4EgnHcOqMwlrs2JBBx7aI9OaSJswesr6Apfk="; - }; - packageRequires = [ epkgs.inheritenv ]; - }) - (epkgs.melpaBuild { + gptel = { + enable = true; + package = gptel; + command = ["gptel"]; + bind."C-c g" = "gptel-menu"; + custom = { + gptel-default-mode = "'org-mode"; + gptel-model = "'claude-sonnet-5"; + gptel-org-branching-context = true; + gptel-include-reasoning = false; + }; + config = '' + <> + ''; + }; + + gptel-agent = { + enable = true; + package = (epkgs: epkgs.melpaBuild { pname = "gptel-agent"; version = "20260823"; src = pkgs.fetchFromGitHub { @@ -852,58 +875,89 @@ Actually, nix *really* requires ~bash~. This is a plugin that allows using zsh f recipe = pkgs.writeText "gptel-agent-recipe" '' (gptel-agent :fetcher github :repo "karthink/gptel-agent" :files (:defaults "agents")) ''; - packageRequires = [ gptel ]; - }) - gptel - ]; + packageRequires = [ (gptel epkgs) ]; + }); + after = ["gptel"]; + config = '' + <> + ''; + }; }; - } + }) #+END_SRC -#+BEGIN_SRC elisp - (use-package claude-code - :bind-keymap ("C-c c" . claude-code-command-map) - :custom (claude-code-optimize-window-resize nil)) +#+BEGIN_SRC elisp :tangle no :noweb-ref emacs-gptel-config + (setf (alist-get 'org-mode gptel-prompt-prefix-alist) "@user\n") + (setf (alist-get 'org-mode gptel-response-prefix-alist) "@assistant\n") + (gptel-make-tool + :name "git_uncommitted_diff" + :function (lambda () + (magit-git-output "diff" "HEAD")) + :description "Return a unified diff of all uncommitted changes (staged and + unstaged) in the current git repository, relative to HEAD. Note: this does + not include brand-new untracked files that have never been added to the + index." + :args nil + :category "git") - (use-package gptel - :commands "gptel" - :bind (("C-c g" . gptel-menu)) - :custom - (require 'org) - (gptel-default-mode 'org-mode) - (gptel-model 'claude-sonnet-5)) + (gptel-make-tool + :name "git_head_commit_diff" + :function (lambda () + (magit-git-output "show" "HEAD")) + :description "Return the diff (including commit message) introduced by the + HEAD commit in the current git repository, i.e. =git show HEAD=." + :args nil + :category "git") - (use-package gptel-agent - :after gptel - :config - (require 'gptel-agent-tools-introspection) - (gptel-agent-update) - (gptel-make-preset 'coding - :description "Preset for agentic coding" - :backend "Claude" - :model 'claude-sonnet-5 - :use-tools t - :tools '("Agent" "Skill" "TodoWrite" "Grep" "Read" "Glob" "Write" "Insert" - "Edit" "Mkdir" "Diagnostics" "WebFetch" "WebSearch" "Bash")) - (gptel-make-preset 'research - :description "Preset for answering questions with web results" - :backend "Claude" - :model 'claude-sonnet-5 - :use-tools t - :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"))) + (gptel-make-tool + :name "git_branch_diff" + :function (lambda () + (let* ((upstream (magit-get-upstream-branch)) + (merge-base (and upstream (magit-git-string "merge-base" "HEAD" upstream)))) + (if (not merge-base) + "Could not determine upstream branch / merge base for the current branch." + (magit-git-output "diff" merge-base)))) + :description "Return a unified diff of all committed changes on the current + branch relative to its upstream tracking branch's merge base. This shows + everything this branch has added since it diverged from upstream." + :args nil + :category "git") + +#+END_SRC + +#+BEGIN_SRC elisp :tangle no :noweb-ref emacs-gptel-agent-config + (require 'gptel-agent-tools-introspection) + (gptel-agent-update) + (gptel-make-preset 'coding + :description "Preset for agentic coding" + :backend "Claude" + :model 'claude-sonnet-5 + :use-tools t + :max-tokens 128000 + :tools '("Grep" "Read" "Glob" "Write" "Insert" + "Edit" "Mkdir" "Diagnostics" "WebFetch" "WebSearch" "Bash" "git_uncommitted_diff" "git_head_commit_diff" + "git_branch_diff")) + (gptel-make-preset 'research + :description "Preset for answering questions with web results" + :backend "Claude" + :model 'claude-sonnet-5 + :use-tools t + :max-tokens 128000 + :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 + :max-tokens 128000 + :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 @@ -992,14 +1046,12 @@ These are just some general settings that apply everywhere. First, a few package { programs.emacs = { extraPackages = epkgs: with epkgs; [ - ace-window doom-modeline free-keys - ligature - mixed-pitch - popper ]; }; + + programs.emacs.init.usePackage.emacs.enable = true; } #+END_SRC @@ -1106,36 +1158,52 @@ especially on monitors with a lot of vertical space. From the docs: "If ‘complete’, TAB first tries to indent the current line, and if the line was already indented, then try to complete the thing at point." Recommended by [[https://github.com/minad/corfu][the ~corfu~ readme]]. -#+BEGIN_SRC elisp - (setopt tab-always-indent 'complete) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.emacs.custom.tab-always-indent = "'complete"; + } #+END_SRC Set the cursor to be a bar. I'm not sure what the benefit would be of a block cursor, since the current character isn't really "selected" like it would be in Helix. I switch this to a box when in god mode / read-only buffers, like vim would. -#+BEGIN_SRC elisp - (setq-default cursor-type 'bar) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.emacs.config = '' + (setq-default cursor-type 'bar) + ''; + } #+END_SRC Repeat-mode takes care of eliminating some annoying duplicate keypresses. I mostly use repeat-mode for two-character commands that need to be repeated; ~devil~ also has some single-key-with-modifier repeats set up that only apply in that mode. -#+BEGIN_SRC elisp - (add-hook 'after-init-hook 'repeat-mode) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.repeat = { + enable = true; + hook = ["after-init"]; + }; + } #+END_SRC Save the minibuffer history to disk, so recently-used options show up at the top of the picker across restarts. -#+BEGIN_SRC elisp - (add-hook 'after-init-hook 'savehist-mode) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.savehist = { + enable = true; + hook = ["after-init"]; + }; + } #+END_SRC Let's unbind some builtin keys that I don't use. I'm not sure if I'll replace them with anything or not, but at a minimum it'll avoid accidental keypresses and clean up which-key output. -#+begin_src elisp +#+begin_src elisp :tangle no (require 'bind-key) (unbind-key "C-x RET") (unbind-key "C-x #") @@ -1161,60 +1229,90 @@ it would update after every command, slowing down everything). I don't need the I have the tab bar at the top of the window. The other settings just clear up some space, and make things look at little nicer. -#+BEGIN_SRC elisp - (require 'doom-modeline) - (setopt doom-modeline-irc nil) - (setopt doom-modeline-workspace-name nil) - (setopt doom-modeline-buffer-encoding nil) - (setopt doom-modeline-buffer-file-name-style 'relative-to-project) - ; (setopt doom-modeline-vcs-max-length 30) - (doom-modeline-remove-segment 'vcs 'main) - (doom-modeline-mode) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.doom-modeline = { + enable = true; + hook = ["after-init"]; + custom = { + doom-modeline-irc = false; + doom-modeline-workspace-name = false; + doom-modeline-buffer-encoding = false; + doom-modeline-buffer-file-name-style = "'relative-to-project"; + }; + config = '' + (doom-modeline-remove-segment 'vcs 'main) + ''; + }; + } #+END_SRC Ligature mode combines character sequences like <= into a single on-screen glyph. It looks kind of cool, so I'm turning it on in prog mode. The enabled ligatures are font-specific, taken from [[https://github.com/mickeynp/ligature.el/wiki#jetbrains-mono][the ligature wiki]]. -#+BEGIN_SRC elisp :noweb no - (ligature-set-ligatures 'prog-mode - '("--" "---" "==" "===" "!=" "!==" "=!=" - "=:=" "=/=" "<=" ">=" "&&" "&&&" "&=" "++" "+++" "***" ";;" "!!" - "??" "???" "?:" "?." "?=" "<:" ":<" ":>" ">:" "<:<" "<>" "<<<" ">>>" - "<<" ">>" "||" "-|" "_|_" "|-" "||-" "|=" "||=" "##" "###" "####" - "#{" "#[" "]#" "#(" "#?" "#_" "#_(" "#:" "#!" "#=" "^=" "<$>" "<$" - "$>" "<+>" "<+" "+>" "<*>" "<*" "*>" "" "/>" "" "->" "->>" "<<-" "<-" "<=<" "=<<" "<<=" "<==" "<=>" "<==>" - "==>" "=>" "=>>" ">=>" ">>=" ">>-" ">-" "-<" "-<<" ">->" "<-<" "<-|" - "<=|" "|=>" "|->" "<->" "<~~" "<~" "<~>" "~~" "~~>" "~>" "~-" "-~" - "~@" "[||]" "|]" "[|" "|}" "{|" "[<" ">]" "|>" "<|" "||>" "<||" - "|||>" "<|||" "<|>" "..." ".." ".=" "..<" ".?" "::" ":::" ":=" "::=" - ":?" ":?>" "//" "///" "/*" "*/" "/=" "//=" "/==" "@_" "__" "???" - "<:<" ";;;")) - (global-ligature-mode t) +#+BEGIN_SRC nix :noweb no + { + programs.emacs.init.usePackage.ligature = { + enable = true; + hook = ["(after-init . global-ligature-mode)"]; + config = '' + (ligature-set-ligatures 'prog-mode + '("--" "---" "==" "===" "!=" "!==" "=!=" + "=:=" "=/=" "<=" ">=" "&&" "&&&" "&=" "++" "+++" "***" ";;" "!!" + "??" "???" "?:" "?." "?=" "<:" ":<" ":>" ">:" "<:<" "<>" "<<<" ">>>" + "<<" ">>" "||" "-|" "_|_" "|-" "||-" "|=" "||=" "##" "###" "####" + "#{" "#[" "]#" "#(" "#?" "#_" "#_(" "#:" "#!" "#=" "^=" "<$>" "<$" + "$>" "<+>" "<+" "+>" "<*>" "<*" "*>" "" "/>" "" "->" "->>" "<<-" "<-" "<=<" "=<<" "<<=" "<==" "<=>" "<==>" + "==>" "=>" "=>>" ">=>" ">>=" ">>-" ">-" "-<" "-<<" ">->" "<-<" "<-|" + "<=|" "|=>" "|->" "<->" "<~~" "<~" "<~>" "~~" "~~>" "~>" "~-" "-~" + "~@" "[||]" "|]" "[|" "|}" "{|" "[<" ">]" "|>" "<|" "||>" "<||" + "|||>" "<|||" "<|>" "..." ".." ".=" "..<" ".?" "::" ":::" ":=" "::=" + ":?" ":?>" "//" "///" "/*" "*/" "/=" "//=" "/==" "@_" "__" "???" + "<:<" ";;;")) + ''; + }; + } #+END_SRC Use variable-width fonts in text buffers. ~mixed-pitch-mode~ keeps most code blocks as monospace. -#+BEGIN_SRC elisp - (add-hook 'text-mode-hook 'mixed-pitch-mode) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.mixed-pitch = { + enable = true; + command = ["mixed-pitch-mode"]; + hook = ["text-mode"]; + init = '' + (defun no-mixed-pitch () + (mixed-pitch-mode -1)) - (defun no-mixed-pitch () - (mixed-pitch-mode -1)) - - (add-hook 'yaml-ts-mode-hook 'no-mixed-pitch) - (add-hook 'mhtml-mode 'no-mixed-pitch) - (add-hook 'nxml-mode 'no-mixed-pitch) + (add-hook 'yaml-ts-mode-hook 'no-mixed-pitch) + (add-hook 'mhtml-mode 'no-mixed-pitch) + (add-hook 'nxml-mode 'no-mixed-pitch) + ''; + }; + } #+END_SRC A basic ace-window setup. Allows switching between windows quickly (although avy can do the same...). -#+BEGIN_SRC elisp - (bind-keys ("M-o" . ace-window) - ("s-o" . ace-window) - ("C-c o" . ace-window)) - (setopt aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l)) - (setopt aw-dispatch-always t) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.ace-window = { + enable = true; + bind = { + "M-o" = "ace-window"; + "s-o" = "ace-window"; + "C-c o" = "ace-window"; + }; + custom = { + aw-keys = "'(?a ?s ?d ?f ?g ?h ?j ?k ?l)"; + aw-dispatch-always = true; + }; + }; + } #+END_SRC Set up a few project- / workspace-related key bindings. @@ -1239,23 +1337,13 @@ Set up a few project- / workspace-related key bindings. } #+END_SRC -Don't run a server. I'm not using this, I can always turn it back on if I need to. - -#+BEGIN_SRC elisp - (server-mode -1) -#+END_SRC - All the beeping is annoying. This flashes a huge icon over the screen, which is still annoying, but less so? -#+BEGIN_SRC elisp - (setopt visible-bell t) -#+END_SRC - -I'm trying this out. This disables visual selections. It's bold, which is why I like it :) - -#+BEGIN_SRC elisp - (transient-mark-mode -1) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.emacs.custom.visible-bell = true; + } #+END_SRC I use tabs to manage multiple "workspaces". I don't tie a project to a tab or anything; often fixing @@ -1266,18 +1354,27 @@ fix / issue, and leave it open until I'm done with that issue. change when drilling into a help page or something. I've set up repeat mode to make it easier to move backward / forward through history. -#+BEGIN_SRC elisp - (bind-keys :repeat-map tab-bar-repeat-map - ("" . tab-bar-history-back) - ("" . tab-bar-history-forward)) - - (tab-bar-history-mode) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.tab-bar = { + enable = true; + hook = ["(after-init . tab-bar-history-mode)"]; + config = '' + (bind-keys :repeat-map tab-bar-repeat-map + ("" . tab-bar-history-back) + ("" . tab-bar-history-forward)) + ''; + }; + } #+END_SRC Make compilation mode buffers auto-scroll to the first error: -#+BEGIN_SRC elisp - (setopt compilation-scroll-output 'first-error) +#+BEGIN_SRC nix + { + programs.emacs.init.usePackage.compile.enable = true; + programs.emacs.init.usePackage.compile.custom.compilation-scroll-output = true; + } #+END_SRC #+begin_src nix @@ -1331,7 +1428,7 @@ content to be. }; custom = { popper-display-control = false; - popper-reference-buffers = ''(concat my/bottom-popups my/right-popups)''; + popper-reference-buffers = ''(append jf/bottom-popups jf/right-popups)''; }; }; }; @@ -1367,7 +1464,6 @@ content to be. "\\*claude:.*" "\\*Man .*" "\\*List .*" - "\\*Embark Actions\\*" "\\*eldoc\\*")) #+end_src @@ -1768,7 +1864,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-actions-indications = false; + eglot-code-action-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)";