Blame


1 40ca3ee3 2020-12-28 op I’m starting to enjoy Emacs Lisp (elisp) more and more. I’m not exactly sure why (well, I have a soft spot for LISP, regardless of its incarnation), because I think Common LISP, Clojure or Scheme are better programming languages overall, but in these last months I ended up writing more elisp than anything else.
2 40ca3ee3 2020-12-28 op
3 40ca3ee3 2020-12-28 op The point is, I think, that elisp is incredibly fun. I’ve never seen a LISP machine in real life, so I can’t compare the experience of working there to using Emacs, but the feeling of interactiveness (is that a word? flyspell beg to differ, but it gives the idea) it has is incredible.
4 40ca3ee3 2020-12-28 op
5 40ca3ee3 2020-12-28 op You have this interactive system, programmable (and re-programmable) at hand. You can literally change almost any behaviour at runtime, with an immediate feedback. I don’t think I can convey how much fun is Emacs for me.
6 40ca3ee3 2020-12-28 op
7 40ca3ee3 2020-12-28 op You shouldn’t be surprised when you see peoples running almost everything (even their window manager!) inside emacs. The joy it gives being able to adjust and configure ANYTHING, even the smallest detail, with your knowledge and capacity being almost the only limitations, makes you want to live inside it.
8 40ca3ee3 2020-12-28 op
9 40ca3ee3 2020-12-28 op One thing that is sometimes overlooked though, especially by people who haven’t used it, is how Emacs is an interface for your operating system, rather than being a replacement. (Yes, I know the old joke about the OS lacking a decent editor, but it’s a joke, for the most part at least.)
10 40ca3ee3 2020-12-28 op
11 40ca3ee3 2020-12-28 op Emacs makes it really easy to make various part of your system interactive. Two good examples are dired and VC. Dired is the DIRectory EDitor; under the hood it run ls(1) and makes its output interactive, so you can sort files by various means, deleting/renaming and tagging bunch of files at once et cetera. VC is a generic Version Control library that supports various version control systems (git, subversion, RCS, CVS and many more) under an unified interface, it’s really cool.
12 40ca3ee3 2020-12-28 op
13 40ca3ee3 2020-12-28 op I tried, as an exercise to improve my understanding, to write an interactive interface for sndio(8), the OpenBSD audio server. One usually interacts with sndio(8) via sndioctl(1). For instance, to list all the input/outputs you can:
14 40ca3ee3 2020-12-28 op
15 40ca3ee3 2020-12-28 op ``` sndioctl(1) output
16 40ca3ee3 2020-12-28 op $ sndioctl
17 40ca3ee3 2020-12-28 op output.level=0.331
18 40ca3ee3 2020-12-28 op server.device=0
19 40ca3ee3 2020-12-28 op app/aucat0.level=1.000
20 40ca3ee3 2020-12-28 op app/firefox0.level=1.000
21 40ca3ee3 2020-12-28 op app/mpv0.level=1.000
22 40ca3ee3 2020-12-28 op app/mpv1.level=1.000
23 40ca3ee3 2020-12-28 op app/mpv2.level=1.000
24 40ca3ee3 2020-12-28 op ```
25 40ca3ee3 2020-12-28 op
26 40ca3ee3 2020-12-28 op and to adjust the volume of firefox you’ll do something like
27 40ca3ee3 2020-12-28 op
28 40ca3ee3 2020-12-28 op ``` adjusting the volume of firefox with sndioctl
29 40ca3ee3 2020-12-28 op $ sndioctl app/firefox0.level=0.7
30 40ca3ee3 2020-12-28 op output.level=0.701
31 40ca3ee3 2020-12-28 op ```
32 40ca3ee3 2020-12-28 op
33 40ca3ee3 2020-12-28 op you can also do things like increment or decrement by a certain amount, or toggle the mute.
34 40ca3ee3 2020-12-28 op
35 40ca3ee3 2020-12-28 op Obviously, typing the whole app/firefox0.level isn’t the most pleasing thing in the world, so you probably have some sort autocompletion in your shell. Also, if your keyboard has the keys to adjust the volume, is highly possible that they’re working by default (even in the TTY, same story for the luminosity).
36 40ca3ee3 2020-12-28 op
37 40ca3ee3 2020-12-28 op Without further ado, let’s dig into the implementation of sndio.el. Like for my text/gemini parser in Clojure, I’m going to try to transform this post into a literate programming exercise. That is, if you tangle (put together) all the elisp blocks of code, you should end up with the same file I’m using (modulo some comments).
38 40ca3ee3 2020-12-28 op
39 40ca3ee3 2020-12-28 op Even if lexical binding is the default on Emacs 27, enable it. And, while we’re there, also write a top comment as is usually done in elisp-land.
40 40ca3ee3 2020-12-28 op
41 40ca3ee3 2020-12-28 op ```elisp
42 40ca3ee3 2020-12-28 op ;;; sndio.el --- Interact with sndio(8) -*- lexical-binding: t; -*-
43 40ca3ee3 2020-12-28 op
44 40ca3ee3 2020-12-28 op (eval-when-compile (require 'subr-x))
45 40ca3ee3 2020-12-28 op ```
46 40ca3ee3 2020-12-28 op
47 40ca3ee3 2020-12-28 op subr-x gives us access to some macros, like when-let, so it’s nice to have. Then we define some variables that we’ll use later.
48 40ca3ee3 2020-12-28 op
49 40ca3ee3 2020-12-28 op ```elisp
50 40ca3ee3 2020-12-28 op (defvar sndio-sndioctl-cmd "sndioctl"
51 40ca3ee3 2020-12-28 op "Path to the sndioctl executable.")
52 40ca3ee3 2020-12-28 op
53 40ca3ee3 2020-12-28 op (defvar sndio-step 0.02
54 40ca3ee3 2020-12-28 op "Step for `sndio-increase' and `sndio-decrease'.")
55 40ca3ee3 2020-12-28 op ```
56 40ca3ee3 2020-12-28 op
57 40ca3ee3 2020-12-28 op Buffers are one of the fundamental blocks of Emacs. Buffers are places where you can store data. When you open (visit in Emacs parlance) a file you get a buffer with the content of the file. Likewise, when you write something interactive, you need a place where to show things, and you do this in a buffer. Buffers are associated with a major mode, and can have an unlimited number of minor modes. You should probably check the manual to learn the differences between these, but the gist of it is that a major mode is a chunk of code that govern how you interact with that particular buffer. For instance, when you visit a C file the major mode cc-mode provides indentation and syntax highlighting, amongst other things. Minor modes are instead more like utility stuff, they usually implement functionalities that are useful in more situations.
58 40ca3ee3 2020-12-28 op
59 40ca3ee3 2020-12-28 op Every mode has a keymap. A keymap like a dictionary where you associate keybindings to functions. Since we’re gonna define a sndio-mode, we need a keymap for it
60 40ca3ee3 2020-12-28 op
61 40ca3ee3 2020-12-28 op ```elisp
62 40ca3ee3 2020-12-28 op (defvar sndio-mode-map
63 40ca3ee3 2020-12-28 op (let ((m (make-sparse-keymap)))
64 40ca3ee3 2020-12-28 op (define-key m (kbd "n") #'forward-line)
65 40ca3ee3 2020-12-28 op (define-key m (kbd "p") #'previous-line)
66 40ca3ee3 2020-12-28 op (define-key m (kbd "i") #'sndio-increase)
67 40ca3ee3 2020-12-28 op (define-key m (kbd "d") #'sndio-decrease)
68 40ca3ee3 2020-12-28 op (define-key m (kbd "m") #'sndio-mute)
69 40ca3ee3 2020-12-28 op (define-key m (kbd "t") #'sndio-toggle)
70 40ca3ee3 2020-12-28 op (define-key m (kbd "g") #'sndio-update)
71 40ca3ee3 2020-12-28 op m)
72 40ca3ee3 2020-12-28 op "Keymap for sndio.")
73 40ca3ee3 2020-12-28 op ```
74 40ca3ee3 2020-12-28 op
75 40ca3ee3 2020-12-28 op There are two ways to create a keymap, make-sparse-keymap and make-keymap. A sparse keymap should be more optimised towards a small amount of keys, but the distinction is probably an historical accident. The majority of the time you want to create a sparse keymap. We’ll see the definition of the functions, except for forward-line and previous-line that are built in, in a moment.
76 40ca3ee3 2020-12-28 op
77 40ca3ee3 2020-12-28 op ---
78 40ca3ee3 2020-12-28 op
79 40ca3ee3 2020-12-28 op Aside: elisp, like common lisp, is a LISP-2, while Clojure and Scheme are LISP-1. LISP-1 has the same semantics as most other programming languages like C, Haskell, Java etc, so if you come from one of these languages you may find LISP-1 more simple to grasp and LISP-2 languages a bit funny. The difference is that in LISP-2 a symbol has different semantics based on the place it is: if it’s the first element in a list, is considered a function, otherwise is considered a variable. This means that it’s legal to write something like
80 40ca3ee3 2020-12-28 op
81 40ca3ee3 2020-12-28 op ```common-lisp
82 40ca3ee3 2020-12-28 op (let ((list (list 1 2 3)))
83 40ca3ee3 2020-12-28 op (list list list)) ; ok, probably a bad example
84 40ca3ee3 2020-12-28 op ;; => ((1 2 3) (1 2 3))
85 40ca3ee3 2020-12-28 op ```
86 40ca3ee3 2020-12-28 op
87 40ca3ee3 2020-12-28 op where list is both a (built-in) function and a variable. Elisp is a LISP-2, so to pass a function as an argument to another you need to sharp-quote it #'like-this. This tells the language that you really mean the function like-this and not the value of a variable named like-this.
88 40ca3ee3 2020-12-28 op
89 40ca3ee3 2020-12-28 op ---
90 40ca3ee3 2020-12-28 op
91 40ca3ee3 2020-12-28 op We have a keymap, so let’s create a mode. We define a sndio-mode that derives from special-mode. special-mode is a built-in mode meant for interactive buffers. Like inheriting in OOP, deriving from a mode lets us get various things for free. For instance, special-mode makes the buffer read only, so when the user press a key it doesn’t get inserted into the buffer. (For the record, most programming language inherits from prog-mode, so it’s easier for the user to enable stuff for all programming-related buffers.)
92 40ca3ee3 2020-12-28 op
93 40ca3ee3 2020-12-28 op ```elisp
94 40ca3ee3 2020-12-28 op (define-derived-mode sndio-mode special-mode "sndio"
95 40ca3ee3 2020-12-28 op "Major mode for sndio interaction."
96 40ca3ee3 2020-12-28 op (buffer-disable-undo)
97 40ca3ee3 2020-12-28 op (sndio-update))
98 40ca3ee3 2020-12-28 op ```
99 40ca3ee3 2020-12-28 op
100 40ca3ee3 2020-12-28 op We also call sndio-update as a step of activating the major mode. sndio-update will capture the output of sndioctl.
101 40ca3ee3 2020-12-28 op
102 40ca3ee3 2020-12-28 op ```elisp
103 40ca3ee3 2020-12-28 op (defun sndio-update ()
104 40ca3ee3 2020-12-28 op "Update the current sndio buffer."
105 40ca3ee3 2020-12-28 op (interactive)
106 40ca3ee3 2020-12-28 op (with-current-buffer "*sndio*"
107 40ca3ee3 2020-12-28 op (let ((inhibit-read-only t))
108 40ca3ee3 2020-12-28 op (erase-buffer)
109 40ca3ee3 2020-12-28 op (process-file sndio-sndioctl-cmd nil (current-buffer) nil))))
110 40ca3ee3 2020-12-28 op ```
111 40ca3ee3 2020-12-28 op
112 40ca3ee3 2020-12-28 op It does so by making sure we are in the “*sndio*” buffer, then disables the read-only status of the buffer so it can erase it and capture the output of sndio-sndioctl-cmd (i.e. “sndioctl”).
113 40ca3ee3 2020-12-28 op
114 40ca3ee3 2020-12-28 op Aside: elisp has lexical binding, but variables defined with defvar are special and are subject to dynamic binding. inhibit-read-only is one of such vars.
115 40ca3ee3 2020-12-28 op
116 40ca3ee3 2020-12-28 op Another interesting thing is the interactive signature. That signature tells Emacs that function can be called interactively (either by binding it to a key or via M-x). Not all functions can be called interactively (and for most function it doesn’t make sense, what should do #’+ when called interactively?), so we need to explicitly mark those functions.
117 40ca3ee3 2020-12-28 op
118 40ca3ee3 2020-12-28 op Then we define three helper functions (note that these aren’t interactive). sndio--run is a wrapper around #’process-file: it calls sndioctl with the given arguments and return its output as string.
119 40ca3ee3 2020-12-28 op
120 40ca3ee3 2020-12-28 op ```elisp
121 40ca3ee3 2020-12-28 op (defun sndio--run (&rest args)
122 40ca3ee3 2020-12-28 op "Run `sndio-sndioctl-cmd' with ARGS yielding its output."
123 40ca3ee3 2020-12-28 op (with-temp-buffer
124 40ca3ee3 2020-12-28 op (when (zerop (apply #'process-file sndio-sndioctl-cmd nil t nil args))
125 40ca3ee3 2020-12-28 op (buffer-string))))
126 40ca3ee3 2020-12-28 op ```
127 40ca3ee3 2020-12-28 op
128 40ca3ee3 2020-12-28 op Aside: note how this function has two dash - in its name. elisp doesn’t have namespaces, and thus it doesn’t have the concept of private and public functions. Every function is public. So, where the language doesn’t arrive, conventions do: two dash means a “private” function.
129 40ca3ee3 2020-12-28 op
130 40ca3ee3 2020-12-28 op sndio--current-io returns the name of the channel where the cursor (point in Emacs parlance) is.
131 40ca3ee3 2020-12-28 op
132 40ca3ee3 2020-12-28 op ```
133 40ca3ee3 2020-12-28 op (defun sndio--current-io ()
134 40ca3ee3 2020-12-28 op "Yield the input/poutput at point as string."
135 40ca3ee3 2020-12-28 op (when-let (end (save-excursion
136 40ca3ee3 2020-12-28 op (beginning-of-line)
137 40ca3ee3 2020-12-28 op (ignore-errors (search-forward "="))))
138 40ca3ee3 2020-12-28 op (buffer-substring-no-properties (line-beginning-position)
139 40ca3ee3 2020-12-28 op (1- end))))
140 40ca3ee3 2020-12-28 op ```
141 40ca3ee3 2020-12-28 op
142 40ca3ee3 2020-12-28 op sndio--update-value updates the value for the channel where the point is.
143 40ca3ee3 2020-12-28 op
144 40ca3ee3 2020-12-28 op ```elisp
145 40ca3ee3 2020-12-28 op (defun sndio--update-value (x)
146 40ca3ee3 2020-12-28 op "Update the value for the input/output at point setting it to X."
147 40ca3ee3 2020-12-28 op (save-excursion
148 40ca3ee3 2020-12-28 op (beginning-of-line)
149 40ca3ee3 2020-12-28 op (search-forward "=")
150 40ca3ee3 2020-12-28 op (let ((inhibit-read-only t))
151 40ca3ee3 2020-12-28 op (delete-region (point) (line-end-position))
152 40ca3ee3 2020-12-28 op (insert (string-trim-right x)))))
153 40ca3ee3 2020-12-28 op ```
154 40ca3ee3 2020-12-28 op
155 40ca3ee3 2020-12-28 op save-excursion restore the position of the point (and other things) after its body completed.
156 40ca3ee3 2020-12-28 op
157 40ca3ee3 2020-12-28 op We’re near the end. Now we’ll define the interactive function to increase/decrease/mute and toggle the input/output channel at point using what we wrote earlier. We’ll use the concat function to concatenate into a string and build the arguments to sndioctl.
158 40ca3ee3 2020-12-28 op
159 40ca3ee3 2020-12-28 op ```elisp
160 40ca3ee3 2020-12-28 op (defun sndio-increase ()
161 40ca3ee3 2020-12-28 op "Increase the volume for the input/output at point."
162 40ca3ee3 2020-12-28 op (interactive)
163 40ca3ee3 2020-12-28 op (when-let (x (sndio--current-io))
164 40ca3ee3 2020-12-28 op (when-let (val (sndio--run "-n" (concat x "=+" (number-to-string sndio-step))))
165 40ca3ee3 2020-12-28 op (sndio--update-value val))))
166 40ca3ee3 2020-12-28 op
167 40ca3ee3 2020-12-28 op (defun sndio-decrease ()
168 40ca3ee3 2020-12-28 op "Decrease the volume for the input/output at point."
169 40ca3ee3 2020-12-28 op (interactive)
170 40ca3ee3 2020-12-28 op (when-let (x (sndio--current-io))
171 40ca3ee3 2020-12-28 op (when-let (val (sndio--run "-n" (concat x "=-" (number-to-string sndio-step))))
172 40ca3ee3 2020-12-28 op (sndio--update-value val))))
173 40ca3ee3 2020-12-28 op
174 40ca3ee3 2020-12-28 op (defun sndio-mute ()
175 40ca3ee3 2020-12-28 op "Mute the input/output at point."
176 40ca3ee3 2020-12-28 op (interactive)
177 40ca3ee3 2020-12-28 op (when-let (x (sndio--current-io))
178 40ca3ee3 2020-12-28 op (when-let (val (sndio--run "-n" (concat x "=0")))
179 40ca3ee3 2020-12-28 op (sndio--update-value val))))
180 40ca3ee3 2020-12-28 op
181 40ca3ee3 2020-12-28 op (defun sndio-toggle ()
182 40ca3ee3 2020-12-28 op "Toggle input/output at point."
183 40ca3ee3 2020-12-28 op (interactive)
184 40ca3ee3 2020-12-28 op (when-let (x (sndio--current-io))
185 40ca3ee3 2020-12-28 op (when-let (val (sndio--run "-n" (concat x "=!")))
186 40ca3ee3 2020-12-28 op (sndio--update-value val))))
187 40ca3ee3 2020-12-28 op ```
188 40ca3ee3 2020-12-28 op
189 40ca3ee3 2020-12-28 op Finally, we define an entry point and conclude our small library:
190 40ca3ee3 2020-12-28 op
191 40ca3ee3 2020-12-28 op ```elisp
192 40ca3ee3 2020-12-28 op (defun sndio ()
193 40ca3ee3 2020-12-28 op "Launch sndio."
194 40ca3ee3 2020-12-28 op (interactive)
195 40ca3ee3 2020-12-28 op (switch-to-buffer "*sndio*")
196 40ca3ee3 2020-12-28 op (sndio-mode))
197 40ca3ee3 2020-12-28 op
198 40ca3ee3 2020-12-28 op (provide 'sndio)
199 40ca3ee3 2020-12-28 op ;;; sndio.el ends here
200 40ca3ee3 2020-12-28 op ```
201 40ca3ee3 2020-12-28 op
202 40ca3ee3 2020-12-28 op sndio is an interactive function that will switch to a buffer named “*sndio*” and activate our major mode. The result is this:
203 40ca3ee3 2020-12-28 op
204 40ca3ee3 2020-12-28 op => /img/sndio-el.gif sndio gif (233K)
205 40ca3ee3 2020-12-28 op
206 40ca3ee3 2020-12-28 op Pretty cool, hu?
207 40ca3ee3 2020-12-28 op
208 40ca3ee3 2020-12-28 op You can find the full code here:
209 40ca3ee3 2020-12-28 op => https://git.omarpolo.com/sndio.el/ git repository
210 40ca3ee3 2020-12-28 op => https://github.com/omar-polo/sndio.el/ GitHub mirror