The Magic of Emacs Comint
Ranked #2,487 in Computers & Electronics, #43,510 overall
A Simple Emacs Comint Subscriber
If you find this lens interesting/useful, please consider logging in and rating in it. Thank you.
Emacs External Processes
Interacting with Asynchronous Processes
A Stock Price Generator
As this is just a simple example, I don't really want to connect to a real stock price feed. Instead, I'll make an example program that generates dummy prices. As most data seems to be passed around as xml these days a price for a particular stock might looks like this:
<data>
<ticker>SADCO</ticker>
<price>101.2191</price>
</data>
Perl Script - ticker.pl
#!/usr/bin/perl
use strict;
use warnings;
use English qw(-no_match_vars);
$OUTPUT_AUTOFLUSH = 1;
my @tickers = qw(MYCO YRCO ANCO ROSCO SADCO);
while (1) {
my $ticker = $tickers[int(rand(5))];
my $price = sprintf "%.4lf", (rand(20) + 90);
print <<"EOF"
<data>
<ticker>$ticker</ticker>
<price>$price</price>
</data>
EOF
sleep 1;
}
Getting Started with Comint
(require 'comint)
(defconst *perl* "/usr/bin/perl")
(defconst *ticker*
(expand-file-name "~/emacs-files/testing/ticker.pl"))
(defun my-subscribe ()
(apply 'make-comint "subscriber" *perl* nil
(list *ticker*)))
Extract the Ticker and the Price
(defconst *my-fields* '(ticker price))
(defconst *my-re-fields*
(format "<\\(%s\\)>\\([^<]*\\)"
(mapconcat (lambda (e) (symbol-name e)) *my-fields* "\\|")))
(defun my-extract-fields (xml)
(let ((pos 0)
(ret-val nil))
(while (string-match *my-re-fields* xml pos)
(push (cons (match-string 1 xml) (match-string 2 xml)) ret-val)
(setq pos (match-end 2)))
ret-val))
Formatting The Output
(defun my-get-value (map key)
(let ((pair (assoc key map)))
(if pair (cdr pair) "undefined")))
(defun my-data-format (fields)
(format "Ticker: %5s Price: %8.4f"
(my-get-value fields "ticker")
(string-to-number (my-get-value fields "price"))))
Planning on Displaying the Output
One mini-gotcha when using the output filters is that emacs doesn't read a line of input at a time. It tries and fills its input buffer as far as it is able. Because of this, we can never be sure we received a complete message so it is best to concatenate all input together until we have what we need. Once we have the complete message we can extract the fields we want and then we need to decide what to do with it.
Now, we could transform the data on the way into the buffer, but I prefer to write the data into a brand new buffer. This way, I can make a mode for the new buffer (if necessary) without deriving from comint. It also makes it easier to debug as I can see exactly what information was received from the external process.
I decided to write a line like the following:
Ticker: MYCO Price: 95.1115
Any price updates for the same ticker will overwrite the original line.
Subscriber Output Filter
The Initial Version
(defvar my-buffer "")
(defun my-output-filter (output)
(setq my-buffer (concat my-buffer output))
(while (string-match "" my-buffer)
(let* ((end (match-end 0))
(xml (substring my-buffer 0 end))
(fields (my-extract-fields xml))
(ticker (my-get-value fields "ticker")))
(when (and output (get-buffer "*display*"))
(with-current-buffer "*display*"
(goto-char (point-min))
(if (re-search-forward (format "Ticker: +%s" ticker) nil t)
(progn
(move-beginning-of-line 1)
(kill-line 1)
(insert (format "%s\n" (my-data-format fields))))
(goto-char (point-max))
(insert (format "%s\n" (my-data-format fields))))))
(setq my-buffer (substring my-buffer end))))
output)
(add-hook 'comint-preoutput-filter-functions 'my-output-filter)
Improving The Output
Highlighting Updates
The Updated Output Filter
(defface my-face-magenta
'((((class color)) (:background "magenta"))
(t (:bold t)))
"my magenta face")
(defun my-add-overlay (begin end face)
(let ((overlay (make-overlay begin end)))
(overlay-put overlay 'face face)
overlay))
(defun my-output-filter (output)
(setq my-buffer (concat my-buffer output))
(while (string-match "" my-buffer)
(let* ((end (match-end 0))
(xml (substring my-buffer 0 end))
(fields (my-extract-fields xml))
(ticker (my-get-value fields "ticker")))
(when (and output (get-buffer "*display*"))
(with-current-buffer "*display*"
(goto-char (point-min))
(if (not (re-search-forward (format "Ticker: +%s" ticker) nil t))
(goto-char (point-max))
(move-beginning-of-line 1)
(kill-line 1))
(let ((begin (point))
(overlay nil))
(insert (format "%s\n" (my-data-format fields)))
(setq overlay (my-add-overlay begin (point) 'my-face-magenta))
(run-with-timer 0.5 nil (lambda (overlay)
(with-current-buffer "*display*"
(delete-overlay overlay)))
overlay))))
(setq my-buffer (substring my-buffer end))))
output)
Split the Window and Subscribe
Finishing Touches
(defun my-unsubscribe ()
(with-current-buffer "*subscriber*"
(comint-kill-subjob)))
(defun my-config-display ()
(delete-other-windows)
(switch-to-buffer-other-window "*display*")
(erase-buffer)
(other-window -1))
(my-config-display)
(my-subscribe)
(my-unsubscribe)
The Emacs Blog
Check Out My Other Emacs Lenses
Comments Or Suggestions?
Please Let Me Know
-
-
Robert McIntyre
Jul 14, 2010 @ 8:13 am | delete
- you need to have a semicolon at the end of your HERE print.
print "EOF" --> print "EOF";
I'm trying to get this to work, so I put this all in my .emacs file, but what do I do to actually get it to do anything?
-
-
-
jareddavison2009
Apr 9, 2009 @ 1:19 am | delete
- Hi Seth, you're welcome. If you have any questions or suggestions for improvements, let me know.
Thanks.
-
-
-
Seth
Apr 6, 2009 @ 11:54 am | delete
- This is great. Thanks for writing this.
I'm in the midst of trying to do something similar so you just saved this elisp beginner a whole bunch of time.
-
by jareddavison2009
My Emacs Lenses:
Creating An Emacs Command
Emacs Hooks - An Introduction
All About Ediff
more »
- 5 featured lenses
- Winner of 3 trophies!
- Top lens » The Magic of Emacs Comint
Explore related pages
- Ediff - All About The Emacs Diff Comparison Tool Ediff - All About The Emacs Diff Comparison Tool
- VIM the Powerful Text Editor VIM the Powerful Text Editor
- Emacs Hooks - An Introduction Emacs Hooks - An Introduction
- HowTo Query Multiple Databases With Emacs Db Mode HowTo Query Multiple Databases With Emacs Db Mode
- Creating An Emacs Command Creating An Emacs Command