diff --git a/config.org b/config.org index ecaf9f4..e0459f4 100644 --- a/config.org +++ b/config.org @@ -1397,6 +1397,186 @@ (setq Man-notify-method 'pushy) #+end_src +** Script-Fu Mode + GIMP has a scheme-based language -- Script-Fu -- built into it that + you can use to script things (based). Sadly, the built-in console + is rather lackluster as a coding environment. Happily, there /is/ + an option to run a server which listens for Script-Fu commands on a + TCP port, so I can use =comint= to make my own lil interface in + Emacs. + + It's things like this that make me really glad I switched to Emacs + because this is ridiculously cool. By my definition of "cool" + anyway -- what can I say, I'm a massive nerd. + + I should probably extract this and make a standalone package out of + it and stick it on Melpa at some point. + +*** REPL Mode + The Script-Fu server request format is very simple: + + | Bytes | Description | + |-------+-----------------------------------------------| + | 0 | 'G' magic byte (47h) | + | 1-2 | Length of expression (BE 32-bit unsigned int) | + | 3+ | Expression | + + Writing an encoder for this is pretty trivial: + + #+begin_src emacs-lisp + (defun script-fu-repl-encode-request (input) + (let* ((len (length input)) + (hi (logand (lsh len -8) #xff)) + (lo (logand len #xff)) + (hdr (vector ?G hi lo))) + (vconcat hdr (encode-coding-string input 'utf-8)))) + #+end_src + + We then want a sender function to use with [[help:comint-mode][comint-mode]] that + applies this encoding. Unfortunately, it seems that there is no + =comint-send-bytes= or similar function to directly send a byte + vector to the comint process. I did try just sending the request + as a string with some invalid characters at the start but ran into + issues: Emacs would sometimes insert unicode control characters + into the data, which GIMP understandably didn't appreciate. + + The method I ended up with is to create a temporary, unibyte + buffer, stick the data in there and then use [[help:comint-send-region][comint-send-region]] to + send the data. It's a bit of a kludge but it seems like it should + be reasonably robust. + + #+begin_src emacs-lisp + (defun script-fu-repl-comint-send-bytes (proc bytes) + (let ((temp-buffer (generate-new-buffer "*script-fu-repl-tmp*"))) + (unwind-protect + (with-current-buffer temp-buffer + (set-buffer-multibyte nil) + (insert (apply #'string (append bytes nil))) + (comint-send-region proc (point-min) (point-max))) + (kill-buffer temp-buffer)))) + #+end_src + + With that handled, implementing the sender function itself is nice + and easy: + + #+begin_src emacs-lisp + (defun script-fu-repl-send (proc input) + (let ((request (script-fu-repl-encode-request input))) + (script-fu-repl-comint-send-bytes proc request))) + #+end_src + + The response format is similarly simple: + + | Bytes | Content | + |-------+-----------------------------------------| + | 0 | 'G' magic byte (47h) | + | 1 | Status code -- 0 on success, 1 on error | + | 2-3 | Length of response text | + | 4 | Response text | + + For now, we only care about the response text, so all we need to do + is skip the first 4 bytes and add a trailing newline. + + #+begin_src emacs-lisp + (defun script-fu-repl-decode-response (response) + (concat (substring response 4) "\n")) + #+end_src + + Another thing is adding a prompt to the comint buffer -- the + server doesn't send one, so we have to add it ourselves. + + #+begin_src emacs-lisp + (defvar script-fu-repl-prompt "> ") + (defun script-fu-repl-insert-prompt (output) + (unless (string-blank-p output) + (let ((proc (get-buffer-process (current-buffer)))) + (goto-char (process-mark proc)) + (unless (looking-back script-fu-repl-prompt) + (insert script-fu-repl-prompt) + (set-marker (process-mark proc) (point))))) + output) + #+end_src + + A mode for the client buffer can then be derived from [[help:comint-mode][comint-mode]]. + + #+begin_src emacs-lisp + (define-derived-mode script-fu-repl-mode comint-mode "Script-Fu REPL" + (setq-local comint-prompt-read-only t) + (setq-local comint-prompt-regexp nil) + (setq-local comint-input-sender #'script-fu-repl-send) + (add-hook 'comint-preoutput-filter-functions + 'script-fu-repl-decode-response nil t) + (add-hook 'comint-output-filter-functions + 'script-fu-repl-insert-prompt nil t)) + #+end_src + + Now, to create a function to create or get the current REPL + buffer. The [[help:comint-check-proc][comint-check-proc]] function can be used to test + whether the buffer is already set up. Rather nicely, + [[help:make-comint-in-buffer][make-comint-in-buffer]] supports passing a ~(HOST . SERVICE)~ pair + to specify a TCP connection to open (via [[help:open-network-stream][open-network-stream]]) so + this is pretty simple. In both cases, we want to return the + client buffer for the caller to use. + + #+begin_src emacs-lisp + (defvar script-fu-repl-server '("localhost" . 10008)) + (defun script-fu-repl () + (interactive) + (let ((buffer (get-buffer-create "*Script-Fu REPL*"))) + (when (not (comint-check-proc buffer)) + (make-comint-in-buffer "Script-Fu REPL" buffer + script-fu-repl-server) + (with-current-buffer buffer (script-fu-repl-mode))) + (pop-to-buffer buffer '((display-buffer-in-direction) + (direction . below) + (window-height . 0.3))) + buffer)) + #+end_src + +*** Code Editing Mode + With the client stuff done, we can define the code editing mode: + + #+begin_src emacs-lisp + (define-derived-mode script-fu-mode scheme-mode "Script-Fu") + #+end_src + + Now to define something to send an expression or region to the + REPL: + + #+begin_src emacs-lisp + (defun script-fu-mode-send-region-or-sexp () + (interactive) + (let ((code (if (use-region-p) + (let ((start (region-beginning)) + (end (region-end))) + (buffer-substring-no-properties start end)) + (thing-at-point 'sexp t)))) + (if (not code) (message "No code to send.") + (let* ((repl-buffer (script-fu-repl)) + (repl-proc (get-buffer-process repl-buffer))) + (script-fu-repl-send repl-proc code))))) + + (define-key script-fu-mode-map (kbd "C-c C-c") + 'script-fu-mode-send-region-or-sexp) + #+end_src + + And finally a similar thing for the whole file: + + #+begin_src emacs-lisp + (defun script-fu-mode-send-file () + (interactive) + (let* ((repl-buffer (script-fu-repl)) + (repl-proc (get-buffer-process repl-buffer)) + (buffer-contents + (buffer-substring-no-properties (point-min) + (point-max)))) + (script-fu-repl-send repl-proc buffer-contents))) + (define-key script-fu-mode-map (kbd "C-c C-l") + 'script-fu-mode-send-file) + #+end_src + + I think that's all I need for now! + * Backup and Autosave ** Keep $PWD Tidy Emacs' default behaviour of dumping temporary files in the current