For a project I’ve been recently working on, I needed Emacs to interact with an external process, sending things to it via its stdin
and receiving responses via its stdout
. I never did anything like that before, so I started with checking my options. I heard good things about Comint mode, but after poking around I decided that it’s not what I’m looking for:
[…] Comint mode, a general-purpose mode for communicating with interactive subprocesses.
Since I explicitly do not want the user to interact with the external process, Comint mode is not suitable for this project.
I asked on the Emacs mailing list, and got several helpful answers; again, Eli Zaretskii turned out to be a helpful hero he is.
So, let’s first code a toy example to see how this is done; in the near future, I’m going to show a real-world application. Let’s forget that Emacs has an insanely advanced scientific calculator built in and make a simple interface to bc.
We need (at the very least) to do three things: start the bc
process, learn to send requests to it, and accept its responses. (To make things simpler, we will associate a buffer with the process so that all bc
responses will be gathered there.) Optionally, we can also restart it when it dies.
(defvar bc-process nil "The `bc' process.") (defun bc-start-process () "Start `bc' in the background unless it's already started." (interactive) (unless (process-live-p bc-process) (setq bc-process (start-process "bc" (get-buffer-create "*bc*") "bc" "-q"))))
So far, it’s pretty clear – we define a variable to store the bc
process and create a command to start it (but do nothing if it is already alive).
Let’s make a command to send user-provided input to bc
. Note that we need to explicitly add a newline to the string sent.
(defun bc-send-input (bc-input) "Send INPUT to `bc-process'." (interactive "sbc input: \n") (if (process-live-p bc-process) (process-send-string bc-process (concat bc-input "\n")) (user-error "`bc' process is not alive")))
Note that bc-send-input
has one drawback – it asks for the input first and only then makes a check for the aliveness of the bc
process. This makes it simpler; a fix for that would include using a Lisp expression instead of a string in the interactive
clause, probably also introducing a dedicated history variable etc. No need to do that in this simple example/proof of concept.
And now the fun part comes. So far, we can manually switch to the *bc*
buffer to see bc
’s reponses – but I’d like to display them in the echo area instead. To achieve that, we need a process filter. It is a function which is called whenever a process sends something to Emacs. By default, internal-default-process-filter
is used, which just inserts the process’ output to its buffer. We want to do the same and to show said output in the echo area. This gets complicated because the process filter may receive partial output. In case of bc
this seems unlikely, but for processes performing more complicated tasks it is possible. (In a future article we will swap bc
for something with more complex output which may indeed come in several parts, and then we’ll be able to really see this phenomenon.) Basically, we need to track two places in the process buffer. One is where we finished processing the previous output – at the same time, it marks the beginning of the new one. The other is where we finished inserting the “current” output – it marks the place we want to insert its next portion. My first idea was that the former one can be handled by the point, and the latter one by process-mark. After some experiments I discovered that the point behaves strangely when the process buffer is visible (and here’s the reason of why it does so), so I decided to use a dedicated marker variable for that in case the user actually looks at the process buffer.
Here is what I’m going to do. The process filter will first insert the received text into the process buffer at process-mark
(like internal-default-process-filter
), then check if the newly inserted material is “complete” (in other words, if there is a newline between bc-process--previous-mark
and process-mark
), and if so, it will gather it, strip the trailing newline, show it in the echo area, and finally move the “previous marker” in the process filter past said newline.
(defvar bc-process--last-mark nil "The end of the previously processed result.") (defun bc-process-filter (process output) "Insert OUTPUT to the buffer of PROCESS. Also, message the user." (let ((buffer (process-buffer process)) (mark (process-mark process))) (when (buffer-live-p buffer) (with-current-buffer buffer (save-excursion (goto-char mark) (insert output) (set-marker mark (point))) (when (save-excursion (search-forward "\n" mark t)) (message "%s" (string-trim-right (buffer-substring bc-process--last-mark mark))) (set-marker bc-process--last-mark mark)))))) (defun bc-start-process () "Start `bc' in the background unless it's already started." (interactive) (unless (process-live-p bc-process) (setq bc-process (start-process "bc" (get-buffer-create "*bc*") "bc" "-q")) (setq bc-process--last-mark (copy-marker (process-mark bc-process))) (set-process-filter bc-process #'bc-process-filter)))
This seems to work fine, but bc
is a very simple program. What if the output from the process can be multiline (well, technically bc
can also output many lines at once, but I just ignored that fact for the sake of simplicity), or arrive in parts, etc.? Well, in the next part we’ll create a simple program which accepts a line of input and outputs a (possibly multiline) JSON object, a line at a time, with some delay, and can even hang or die in the middle of producing an output (so that we can make sure that our Elisp code is capable of handling such errors). For now, that’s all!