Implement Script-Fu mode
This commit is contained in:
parent
76aad81ca2
commit
74af0b81b3
180
config.org
180
config.org
@ -1397,6 +1397,186 @@
|
|||||||
(setq Man-notify-method 'pushy)
|
(setq Man-notify-method 'pushy)
|
||||||
#+end_src
|
#+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
|
* Backup and Autosave
|
||||||
** Keep $PWD Tidy
|
** Keep $PWD Tidy
|
||||||
Emacs' default behaviour of dumping temporary files in the current
|
Emacs' default behaviour of dumping temporary files in the current
|
||||||
|
Loading…
x
Reference in New Issue
Block a user