Skip to content

Commit

Permalink
Add a Texinfo manual to the project.
Browse files Browse the repository at this point in the history
This patch will also add a Github Actions workflow `ci` that will
be run on push, PR, &c.
  • Loading branch information
sp1ff committed Sep 2, 2024
1 parent a563c88 commit dbabcb8
Show file tree
Hide file tree
Showing 9 changed files with 601 additions and 143 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: ci
on:
workflow_dispatch:
pull_request:
types: [opened, edited, reopened] # don't say `synchronize`-- that is taken care of by `push`
push:
schedule:
- cron: '42 02 * * *'

jobs:

# These all seem to run in `/home/runner/work/elmpd/elmpd`
lint_and_test:
name: Lint & Test
strategy:
matrix:
os: [ubuntu-20.04, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:

- name: Checkout repo
uses: actions/checkout@v2

- name: Lint
shell: bash
run: |
set -ex
admin/run-linters
- name: Test
shell: bash
run: |
set -ex
admin/configure-to-distcheck
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ configure
env.sh
*.bz2
*.gz
*.tar
*.xz
*.zst
20 changes: 17 additions & 3 deletions Makefile.am
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
dist_lisp_LISP = elmpd.el
SUBDIRS = test
EXTRA_DIST = README.org
CLEANFILES = .pkg-tmp elmpd-$(PACKAGE_VERSION).tar
SUBDIRS = doc test
AM_ELCFLAGS = --eval '(require (quote bytecomp))'

dist-hook:
$(EMACS) --batch --eval '(checkdoc-file "$(srcdir)/elmpd.el")'
package: elmpd-$(PACKAGE_VERSION).tar $(srcdir)/README.org

srclisp=$(dist_lisp_LISP:%.el=$(srcdir)/%.el)

elmpd-$(PACKAGE_VERSION).tar: $(srclisp) $(srcdir)/README.org
mkdir -p .pkg-tmp/elmpd-$(PACKAGE_VERSION)/ && \
cp $(srclisp) .pkg-tmp/elmpd-$(PACKAGE_VERSION)/ && \
cp $(srcdir)/README.org .pkg-tmp/elmpd-$(PACKAGE_VERSION)/ && \
cd .pkg-tmp && tar cf $@ elmpd-$(PACKAGE_VERSION)/ && \
cd .. && mv -v .pkg-tmp/elmpd-$(PACKAGE_VERSION).tar . && \
rm -rf .pkg-tmp

dist-hook: package
151 changes: 13 additions & 138 deletions README.org
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#+TITLE: elmpd
#+DESCRIPTION: A tight, async mpd library in Emacs Lisp
#+DATE: <2024-08-30 Fri 07:47>
#+DATE: <2024-09-01 Sun 17:10>
#+AUTHOR: sp1ff
#+EMAIL: [email protected]
#+AUTODATE: t
Expand All @@ -13,13 +13,22 @@
* Introduction

[[https://github.com/sp1ff/elmpd][elmpd]] is a tight, asynchronous, ergonomic [[https://www.musicpd.org/][MPD]] client library in Emacs Lisp.
* License

This package is released under the [[https://www.gnu.org/licenses/gpl-3.0.en.html][GPL v3]].
* Prerequisites

Emacs 25.1.
* Installing

The simplest way to install [[https://github.com/sp1ff/elmpd][elmpd]] is from [[https://melpa.org][MELPA]].

You can also install the package manually. Download the .tar file from [[https://github.com/sp1ff/elmpd/releases][Github]] or my personal [[https://www.unwoundstack/distros.html][page]] and say:

#+BEGIN_SRC elisp
(package-install-file "elmpd-0.3.0.tar")
#+END_SRC

I'm now making GitHub releases that include Autotools source distributions:

#+BEGIN_SRC bash
Expand All @@ -40,142 +49,8 @@ sudo make install
#+END_SRC
* Getting Started

** Creating Connections
:PROPERTIES:
:CUSTOM_ID: creating_connections
:END:

Create an MPD connection by calling =elmpd-connect=; this will return an =elmpd-connection= instance immediately. Asynchronously, it will be parsing the MPD greeting message, perhaps sending an initial password, and if so requested, sending the "idle" command.

There are two idioms I've seen in MPD client libraries for sending commands while receiving notifications of server-side changes:

1. just maintain two connections (e.g. [[https://github.com/vincent-petithory/mpdfav][mpdfav]]); issue the "idle" command on one, send commands on the other
2. use one connection, issue the "idle" command, and when asked to issue another command, send "noidle", issue the requested command, collect the response, and then send "idle" again (e.g. [[https://gitea.petton.fr/mpdel/libmpdel][libmpdel]]). Note that this is not a race condition per the MPD [[https://www.musicpd.org/doc/html/protocol.html#idle][docs]] -- any server-side changes that took place while processing the command will be saved & returned on "idle"

Since =elmpd= is a library, I do not make that choice here, but rather support both styles. See the docstring for =elmpd-connect= on how to configure your new connection in either way.

The implementation is callback-based; each connection comes at the cost of a single socket plus whatever memory is needed to do the text processing in handling responses. In particular, I declined to use =tq= despite the natural fit because I didn't want to use a buffer for each connection, as well.
** Invoking Commands

Send commands via =elmpd-send=. For example, to start MPD playing:

#+BEGIN_SRC elisp
(let ((conn (elmpd-connect :host "localhost")))
(elmpd-send conn "play"))
#+END_SRC

sends the "play" command to your MPD server listening on port 6600 on localhost. Note that this code will likely return *before* anything actually happens. As mentioned [[#creating_connections][above]], =elmpd-connect= returns immediately after creating the network process; it only reads & parses the MPD greeting asynchronously. Likewise, =elmpd-send= only queues up the "play" command; it will actually be sent & its response read in the background.

If you'd like to do something with the response, you can provide a callback:

#+BEGIN_SRC elisp
(let ((conn (elmpd-connect :host "localhost")))
(elmpd-send
conn
"getvol"
(lambda (_conn ok rsp)
(if ok
(message "volume is %s" (substring rsp 7))
(error "Failed to get volume: %s" rsp)))))
#+END_SRC

You can send command lists by specifying a list rather than a string as the second parameter:

#+BEGIN_SRC elisp
(let ((conn (elmpd-connect :host "localhost")))
(elmpd-send
conn
'("random 1" "consume 1" "crossfade 5" "play")))
#+END_SRC

Will send the following to your local MPD daemon:

#+BEGIN_EXAMPLE
command_list_begin
random 1
consume 1
crossfade 5
play
command_list_end
#+END_EXAMPLE

Now that we're sending multiple commands, we may be interested in processing the responses in different ways. For instance:

#+BEGIN_SRC elisp
(let ((conn (elmpd-connect :host "localhost"))
(groups '("Pogues" "Rolling Stones" "Flogging Molly")))
(elmpd-send
conn
(cl-mapc (lambda (x) (format "count \"(Artist =~ '%s')\"" x)) groups)
(lambda (_conn ok rsp)
(if ok
;; `rsp' is a list; one response per command
(cl-mapc
(lambda (x)
(let* ((lines (split x "\n" t))
(line (car lines)))
(message (substring line 7)))))
(error "Error counting: %s" rsp)))
'list))
#+END_SRC

will issue the "count" command in a command list (once for each of "Pogues", "Rolling Stones" & "Flogging Molly"), receive the responses as a list, and process the list. If you just can't wait, you can specify ='stream= instead of ='list=; in this case as soon as a response from a sub-command is available, your callback will be invoked with it:

#+BEGIN_SRC elisp
(let ((conn (elmpd-connect :host "localhost"))
(groups '("Pogues" "Rolling Stones" "Flogging Molly")))
(elmpd-send
conn
(cl-mapc (lambda (x) (format "count \"(Artist =~ '%s')\"" x)) groups)
(lambda (_conn ok rsp)
(if ok
;; `rsp' is a string; one invocation per command
(let* ((lines (split x "\n" t))
(line (car lines)))
(message (substring line 7)))
(error "Error counting: %s" rsp)))
'stream))
#+END_SRC

Prior to 0.2.2, sending a subsequent response meant you had to invoke =elmpd-send= from your callback, like so:

#+BEGIN_SRC elisp
(let ((conn (elmpd-connect :host "localhost")))
(elmpd-send
conn
"getvol"
(lambda (_conn ok rsp)
(if ok
(let ((vol (string-to-number (substring rsp 7 -1))))
(if (< vol 50)
(elmpd-send
conn
"setvol 50"
(lambda (_conn ok rsp)
(if ok
(message "Increased volume from %d to 50." vol)
(message "Failed to increase volume: %s" rsp))))))
(error "Failed to get volume: %s" rsp)))))
#+END_SRC

As with Javascript futures, this quickly became inconvenient & difficult to read, so I introduced the =elmpd-chain= macro in the hopes of achieving a syntax more like async Rust:

#+BEGIN_SRC elisp
(let ((conn (elmpd-connect :host "localhost"))
(vol 0))
(elmpd-chain
conn
("getvol"
(lambda (_conn rsp)
(setq vol (string-to-number (substring rsp 7 -1)))))
:or-else
(lambda (_conn rsp) (error "Failed to get volume: %s" rsp))
:and-then
((format "setvol %d" (max 50 vol))
(lambda (_ _) (message "Set volume to %d." vol)))
:or-else
(message "Failed to increase volume: %s" rsp)))
#+END_SRC
User documentation is provided with the package, and may also be found
[[https://unwoundstack.com/doc/elmpd/curr/elmpd.html][here]].
* Motivation & Design Philosphy

[[https://github.com/DamienCassou][Damien Cassou]], the author of [[https://github.com/mpdel/mpdel][mpdel]] and [[https://gitea.petton.fr/mpdel/libmpdel][libmpdel]], [[https://github.com/sp1ff/elmpd/issues/1][reached out]] to ask "Why elmpd?" His question prompted me to clarify my thoughts around this project & I've adapted my response here.
Expand All @@ -185,7 +60,7 @@ I've looked at a few [[https://www.musicpd.org/][MPD]] clients, including [[http
My next move was to read through a number of client libraries for inspiration, both in C & Emacs LISP. Many of them had strong opinions on how one should talk to MPD. Having been programming MPD for a while I had come to appreciate its simplicity (after all, one can program it from bash by simply =echo=-ing commands to =/dev/tcp/$host/$port=). My experience with async Rust inspired me to see how simple I could make this using just callbacks. =elmpd= exports two primary functions: =elmpd-connect= & =elmpd-send=. Each connection consumes a socket & optionally a callback-- that's it (no buffer, no transaction queue). Put another way, if other libraries are Gnus (featureful, encourages you to read your e-mail in a certain way), then elmpd is [[https://mailutils.org/][Mailutils]] (small utilities that leave it up to the user to assemble them into something useful).
* Status & Roadmap

I've been using the library for some time with good results. The bulk of the work has been in getting the asynchronous logic right; as such it is not very featureful. It is ripe for being used to build up a more caller-friendly API: =(something-play)= instead of:
As of September 2024 I'm calling this "1.0". The package is ripe for being used to build up a more caller-friendly API: =(something-play)= instead of:

#+BEGIN_SRC elisp
(let ((conn (elmpd-connect)))
Expand Down
4 changes: 2 additions & 2 deletions configure.ac
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
AC_INIT([elmpd], [0.3.0], [sp1ff@poboxcom], [elmpd], [https://github.com/sp1ff/elmpd])
AC_CONFIG_AUX_DIR([build-aux])
AC_CONFIG_SRCDIR([elmpd.el])
AM_INIT_AUTOMAKE([-Wall -Werror gnits std-options dist-bzip2 dist-xz dist-zstd])
AM_INIT_AUTOMAKE([-Wall -Werror gnits std-options dist-xz dist-zstd])

AM_PATH_LISPDIR

AC_CONFIG_FILES([Makefile test/Makefile])
AC_CONFIG_FILES([Makefile doc/Makefile test/Makefile])
AC_OUTPUT
3 changes: 3 additions & 0 deletions doc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
elmpd.info
stamp-vti
dir
1 change: 1 addition & 0 deletions doc/Makefile.am
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
info_TEXINFOS = elmpd.texi
Loading

0 comments on commit dbabcb8

Please sign in to comment.