diff --git a/README.md b/README.md index a511678..8f82f92 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ An Emacs interface for [Claude Code CLI](https://github.com/anthropics/claude-co - **Seamless Emacs Integration** - Start, manage, and interact with Claude without leaving Emacs - **Stay in Your Buffer** - Send code, regions, or commands to Claude while keeping your focus - **Fix Errors Instantly** - Point at a flycheck/flymake error and ask Claude to fix it +- **Paste Images** - `M-x yank-media` sends a clipboard image to Claude as an `@path` reference (Emacs 29+) - **Multiple Instances** - Run separate Claude sessions for different projects or tasks - **Quick Responses** - Answer Claude with a keystroke (//1/2/3) without switching buffers - **Smart Context** - Optionally include file paths and line numbers when sending commands to Claude @@ -172,6 +173,14 @@ If you put your cursor over a flymake or flycheck error, you can ask Claude to f To show and hide the Claude buffer use `claude-code-toggle` (`C-c c t`). To jump to the Claude buffer use `claude-code-switch-to-buffer` (`C-c c b`). This will open the buffer if hidden. +### Pasting Images + +On Emacs 29 and later, you can paste an image from the system clipboard directly into a Claude buffer with `claude-code-yank-media` (`C-c c p`), or with `M-x yank-media` from inside the Claude buffer. claude-code.el writes the image to a temp file and injects an `@/path/to/image` reference at the prompt; the Claude CLI reads `@path` references natively and attaches the image to your next message. The temp files are cleaned up when the Claude buffer is killed. + +Works with all terminal backends (eat, vterm, ghostel). Disable by setting `claude-code-enable-image-paste` to `nil`, or disable cleanup with `claude-code-image-paste-cleanup-on-kill`. + +If you use the [ghostel](https://github.com/dakra/ghostel) backend, regular `C-v` will also paste clipboard images directly into the Claude buffer — no claude-code.el handling required, since ghostel forwards image data through libghostty as a terminal feature ([thanks @dakra](https://github.com/stevemolitor/claude-code.el/issues/127#issuecomment-4288290963)). Drag-and-drop of image files into the buffer works the same way. + ### Managing Claude Windows The `claude-code-toggle` (`C-c c t`) will show and hide the Claude window. Use the `claude-code-switch-to-buffer` (`C-c c b`) command to switch to the Claude window even if it is hidden. diff --git a/claude-code.el b/claude-code.el index 7d0e0cf..e495f03 100644 --- a/claude-code.el +++ b/claude-code.el @@ -201,6 +201,40 @@ current buffer." :type 'boolean :group 'claude-code-window) +(defcustom claude-code-enable-image-paste t + "Whether to enable image paste via `yank-media' in Claude buffers. + +When non-nil, pasting an image from the system clipboard (or dragging +and dropping an image onto a Claude buffer) via `yank-media' writes the +image to a temp file and inserts an `@/path/to/image' reference at the +prompt. Claude's CLI reads `@path' references natively and will +attach the image to your next message. + +Requires Emacs 29 or later (for `yank-media-handler'). Has no effect +on older Emacs versions." + :type 'boolean + :group 'claude-code) + +(defcustom claude-code-image-paste-cleanup-on-kill t + "Whether to delete pasted image temp files when the Claude buffer is killed. + +When non-nil, any temp files created by `claude-code-enable-image-paste' +are removed when the Claude buffer is killed. When nil, the files +stay in `claude-code-image-paste-directory' until the OS cleans them up." + :type 'boolean + :group 'claude-code) + +(defcustom claude-code-image-paste-directory + (if (file-directory-p "/tmp") "/tmp" temporary-file-directory) + "Directory where pasted images are written. + +Defaults to \"/tmp\" on systems that have it (macOS, Linux, BSD) so +the `@/path/to/image' reference inserted at the prompt stays short and +readable. Falls back to the variable `temporary-file-directory' on +systems (e.g. Windows) without a top-level /tmp." + :type 'directory + :group 'claude-code) + ;;;;; Eat terminal customizations ;; Eat-specific terminal faces (defface claude-code-eat-prompt-annotation-running-face @@ -403,6 +437,7 @@ this history by adding `claude-code-command-history' to (define-key map (kbd "3") 'claude-code-send-3) (define-key map (kbd "M") 'claude-code-cycle-mode) (define-key map (kbd "o") 'claude-code-send-buffer-file) + (define-key map (kbd "p") 'claude-code-yank-media) map) "Keymap for Claude commands.") @@ -433,6 +468,7 @@ this history by adding `claude-code-command-history' to ("x" "Send command with context" claude-code-send-command-with-context) ("r" "Send region or buffer" claude-code-send-region) ("o" "Send buffer file" claude-code-send-buffer-file) + ("p" "Paste image from clipboard" claude-code-yank-media) ("e" "Fix error at point" claude-code-fix-error-at-point) ("f" "Fork conversation" claude-code-fork) ("/" "Slash Commands" claude-code-slash-commands)] @@ -1105,6 +1141,72 @@ BUFFER can be either a buffer object or a buffer name string." (buffer-name buffer)))) (and name (string-match-p "^\\*claude:" name)))) +;;;;; Image paste + +(defvar-local claude-code--pasted-image-files nil + "List of temp image files created by `yank-media' in this buffer. +Cleaned up on `kill-buffer' when `claude-code-image-paste-cleanup-on-kill' +is non-nil.") + +(defconst claude-code--image-mime-extensions + '(("image/png" . ".png") + ("image/jpeg" . ".jpg") + ("image/jpg" . ".jpg") + ("image/gif" . ".gif") + ("image/webp" . ".webp") + ("image/bmp" . ".bmp")) + "Alist mapping image MIME-type prefix to file extension.") + +(defun claude-code--image-extension-for-mimetype (mimetype) + "Return the file extension (including the dot) for MIMETYPE. +MIMETYPE may be a symbol or a string. Falls back to \".png\" if +the type is unrecognized." + (let* ((str (if (symbolp mimetype) (symbol-name mimetype) mimetype)) + (match (seq-find (lambda (pair) (string-prefix-p (car pair) str)) + claude-code--image-mime-extensions))) + (if match (cdr match) ".png"))) + +(defun claude-code--image-yank-media-handler (mimetype data) + "Handle an `image/*' paste in a Claude buffer. +MIMETYPE is the MIME type (string or symbol); DATA is the raw bytes. + +Writes DATA to a temp file named by MIMETYPE's extension, remembers it +for cleanup, then injects `@ ' into the terminal so Claude's CLI +receives it as a file reference at the prompt. + +Returns non-nil to signal the paste was handled." + (let* ((ext (claude-code--image-extension-for-mimetype mimetype)) + (temporary-file-directory claude-code-image-paste-directory) + (path (make-temp-file "claude-image-" nil ext)) + (coding-system-for-write 'binary)) + (with-temp-file path (insert data)) + (push path claude-code--pasted-image-files) + (claude-code--term-send-string claude-code-terminal-backend + (concat "@" path " ")) + (message "Pasted image as %s" path) + t)) + +(defun claude-code--cleanup-pasted-images () + "Delete any temp image files created by `yank-media' in this buffer. +Called from `kill-buffer-hook' when +`claude-code-image-paste-cleanup-on-kill' is non-nil." + (when claude-code-image-paste-cleanup-on-kill + (dolist (file claude-code--pasted-image-files) + (when (file-exists-p file) + (ignore-errors (delete-file file)))) + (setq claude-code--pasted-image-files nil))) + +(defun claude-code--register-image-yank-media-handler () + "Register `yank-media-handler' for images in the current Claude buffer. +No-op on Emacs versions without `yank-media-handler' (pre-29)." + (when (and claude-code-enable-image-paste + (fboundp 'yank-media-handler)) + (yank-media-handler "image/.*" + #'claude-code--image-yank-media-handler) + (add-hook 'kill-buffer-hook + #'claude-code--cleanup-pasted-images + nil t))) + (defun claude-code--directory () "Get get the root Claude directory for the current buffer. @@ -1459,6 +1561,10 @@ With double prefix ARG (\\[universal-argument] \\[universal-argument]), prompt f ;; Add cleanup hook to remove directory mappings when buffer is killed (add-hook 'kill-buffer-hook #'claude-code--cleanup-directory-mapping nil t) + ;; Register yank-media handler so users can paste images from the + ;; clipboard; the handler writes to a temp file and inserts @path. + (claude-code--register-image-yank-media-handler) + ;; run start hooks (run-hooks 'claude-code-start-hook) @@ -1973,6 +2079,22 @@ having to switch to the REPL buffer." (interactive) (claude-code--do-send-command "")) +;;;###autoload +(defun claude-code-yank-media () + "Paste an image from the clipboard into the current Claude buffer. + +Runs `yank-media' in the Claude buffer, which dispatches to the handler +installed by `claude-code--register-image-yank-media-handler': the image +is written to a temp file and an `@/path/to/image' reference is inserted +at the prompt. Claude's CLI reads `@path' references natively. + +Requires Emacs 29 or later." + (interactive) + (unless (fboundp 'yank-media) + (user-error "`yank-media' requires Emacs 29 or later")) + (claude-code--with-buffer + (call-interactively #'yank-media))) + ;;;###autoload (defun claude-code-send-1 () "Send \"1\" to the Claude Code REPL.