1 ;;; ampc.el --- Asynchronous Music Player Controller
3 ;; Copyright (C) 2011-2012 Christopher Schmidt
5 ;; Author: Christopher Schmidt <christopher@ch.ristopher.com>
6 ;; Maintainer: Christopher Schmidt <christopher@ch.ristopher.com>
9 ;; Compatibility: GNU Emacs: 24.x
11 ;; This file is NOT part of GNU Emacs.
13 ;; This program is free software; you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation, either version 3 of the License, or
16 ;; (at your option) any later version.
18 ;; This program is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
27 ;; ampc is a controller for the Music Player Daemon.
30 ;; Put this file somewhere in your load-path or add the directory the file is in
33 ;; (add-to-list 'load-path "~/.emacs.d/ampc")
35 ;; Then add two autoload definitions:
37 ;; (autoload 'ampc "ampc" nil t) (autoload 'ampc-quit "ampc" nil t)
39 ;; Optionally bind keys to these functions, e.g.
41 ;; (global-set-key (kbd "<f9>") 'ampc)
44 ;; To invoke ampc, call the command `ampc', e.g. via M-x ampc RET. Once ampc is
45 ;; connected to the daemon, it creates its window configuration in the selected
46 ;; window. To make ampc use the full frame rather than the selected window,
47 ;; customize `ampc-use-full-frame'.
49 ;; ampc offers three independent views which expose different parts of the user
50 ;; interface. The current playlist view, the default view at startup, may be
51 ;; accessed using the `J' (that is S-j) key. The playlist view may be accessed
52 ;; using the `K' key. The outputs view may be accessed using the `L' key.
54 ;;; *** current playlist view
55 ;; The playlist view should look like this
57 ;; .........................
66 ;; .........................
68 ;; Window one exposes basic information about the daemon, such as the current
69 ;; state (stop/play/pause), the song currently playing, or the volume.
71 ;; All windows, except the status window, contain a tabular list of items. Each
72 ;; item may be selected/marked. There may be multiple selections.
74 ;; To mark an entry, move the point to the entry and press `m' (ampc-mark). To
75 ;; unmark an entry, press `u' (ampc-unmark). To unmark all entries, press `U'
76 ;; (ampc-unmark-all). To toggle marks, press `t' (ampc-toggle-marks). To
77 ;; navigate to the next entry, press `n' (ampc-next-line). Analogous, pressing
78 ;; `p' (ampc-previous-line) moves the point to the previous entry.
80 ;; Window two shows the current playlist. The song that is currently played by
81 ;; the daemon, if any, is highlighted. To delete the selected songs from the
82 ;; playlist, press `d' (ampc-delete). To move the selected songs up, press
83 ;; `<up>' (ampc-up). Analogous, press `<down>' (ampc-down) to move the selected
86 ;; Windows three to five are tag browsers. You use them to narrow the song
87 ;; database to certain songs. Think of tag browsers as filters, analogous to
88 ;; piping `grep' outputs through additional `grep' filters. The property of the
89 ;; songs that is filtered is displayed in the header line of the window.
91 ;; Window six shows the songs that match the filters defined by windows three to
92 ;; five. To add the selected song to the playlist, press `a' (ampc-add). This
93 ;; key binding works in tag browsers as well. Calling ampc-add in a tag browser
94 ;; adds all songs filtered up to the selected browser to the playlist.
97 ;; The playlist view resembles the current playlist view. The window, which
98 ;; exposes the playlist content, is split, though. The bottom half shows a list
99 ;; of stored playlists. The upper half does not expose the current playlist
100 ;; anymore. Instead, the content of the selected (stored) playlist is shown.
101 ;; All commands that used to work in the current playlist view and modify the
102 ;; current playlist now modify the selected (stored) playlist. The list of
103 ;; stored playlists is the only view in ampc that may have only one marked
107 ;; The outputs view contains a single list which shows the configured outputs of
108 ;; mpd. To toggle the enabled property of the selected outputs, press `a'
109 ;; (ampc-toggle-output-enabled).
112 ;; ampc defines the following global keys, which may be used in every window
113 ;; associated with ampc:
115 ;; `k' (ampc-toggle-play): Toggle play state. If mpd does not play a song
116 ;; already, start playing the song at point if the current buffer is the
117 ;; playlist buffer, otherwise start at the beginning of the playlist. With
118 ;; prefix argument 4, stop player rather than pause if applicable.
120 ;; `l' (ampc-next): Play next song.
121 ;; `j' (ampc-previous): Play previous song
123 ;; `c' (ampc-clear): Clear playlist.
124 ;; `s' (ampc-shuffle): Shuffle playlist.
126 ;; `S' (ampc-store): Store playlist.
127 ;; `O' (ampc-load): Load selected playlist in the current playlist.
128 ;; `R' (ampc-rename-playlist): Rename selected playlist.
129 ;; `D' (ampc-delete-playlist): Delete selected playlist.
131 ;; `y' (ampc-increase-volume): Increase volume.
132 ;; `M-y' (ampc-decrease-volume): Decrease volume.
133 ;; `h' (ampc-increase-crossfade): Increase crossfade.
134 ;; `M-h' (ampc-decrease-crossfade): Decrease crossfade.
136 ;; `e' (ampc-toggle-repeat): Toggle repeat state.
137 ;; `r' (ampc-toggle-random): Toggle random state.
138 ;; `f' (ampc-toggle-consume): Toggle consume state.
140 ;; `T' (ampc-trigger-update): Trigger a database update.
141 ;; `q' (ampc-quit): Quit ampc.
147 (require 'network-stream)
153 "Asynchronous client for the Music Player Daemon."
156 :group 'applications)
159 (defcustom ampc-debug nil
160 "Non-nil means log communication between ampc and MPD."
162 (defcustom ampc-use-full-frame nil
163 "If non-nil, ampc will use the entire Emacs screen."
165 (defcustom ampc-truncate-lines t
166 "If non-nil, truncate lines in ampc buffers."
170 (defcustom ampc-before-startup-hook nil
171 "A hook called before startup.
172 This hook is called as the first thing when ampc is started."
174 (defcustom ampc-connected-hook nil
175 "A hook called after ampc connected to MPD."
177 (defcustom ampc-quit-hook nil
178 "A hook called when exiting ampc."
182 (defface ampc-mark-face '((t (:inherit font-lock-constant-face)))
184 (defface ampc-marked-face '((t (:inherit warning)))
185 "Face of marked entries.")
186 (defface ampc-face '((t (:inerhit default)))
187 "Face of unmarked entries.")
188 (defface ampc-current-song-mark-face '((t (:inherit region)))
189 "Face of mark of the current song.")
190 (defface ampc-current-song-marked-face '((t (:inherit region)))
191 "Face of the current song if marked.")
193 ;;; *** internal variables
195 (let ((rs '(1.0 vertical
197 (0.33 tag :tag "Genre" :id 1)
198 (0.33 tag :tag "Artist" :id 2)
199 (1.0 tag :tag "Album" :id 3))
200 (1.0 song :properties (("Track" :title "#")
202 ("Time" :offset 26)))))
204 ("Artist" :offset 20)
206 ("Time" :offset 60))))
211 (1.0 current-playlist :properties ,pl-prop))
218 (0.8 playlist :properties ,pl-prop)
222 outputs :properties (("outputname" :title "Name")
223 ("outputenabled" :title "Enabled" :offset 10))))))
225 (defvar ampc-connection nil)
226 (defvar ampc-outstanding-commands nil)
228 (defvar ampc-working-timer nil)
229 (defvar ampc-yield nil)
231 (defvar ampc-buffers nil)
232 (defvar ampc-buffers-unordered nil)
233 (defvar ampc-all-buffers nil)
235 (defvar ampc-type nil)
236 (make-variable-buffer-local 'ampc-type)
237 (defvar ampc-dirty nil)
238 (make-variable-buffer-local 'ampc-dirty)
240 (defvar ampc-internal-db nil)
241 (defvar ampc-status nil)
244 (defvar ampc-mode-map
245 (let ((map (make-sparse-keymap)))
246 (suppress-keymap map)
247 (define-key map (kbd "k") 'ampc-toggle-play)
248 (define-key map (kbd "l") 'ampc-next)
249 (define-key map (kbd "j") 'ampc-previous)
250 (define-key map (kbd "c") 'ampc-clear)
251 (define-key map (kbd "s") 'ampc-shuffle)
252 (define-key map (kbd "S") 'ampc-store)
253 (define-key map (kbd "O") 'ampc-load)
254 (define-key map (kbd "R") 'ampc-rename-playlist)
255 (define-key map (kbd "D") 'ampc-delete-playlist)
256 (define-key map (kbd "y") 'ampc-increase-volume)
257 (define-key map (kbd "M-y") 'ampc-decrease-volume)
258 (define-key map (kbd "h") 'ampc-increase-crossfade)
259 (define-key map (kbd "M-h") 'ampc-decrease-crossfade)
260 (define-key map (kbd "e") 'ampc-toggle-repeat)
261 (define-key map (kbd "r") 'ampc-toggle-random)
262 (define-key map (kbd "f") 'ampc-toggle-consume)
263 (define-key map (kbd "q") 'ampc-quit)
264 (define-key map (kbd "T") 'ampc-trigger-update)
265 (loop for view in ampc-views
266 do (define-key map (car view)
269 (ampc-configure-frame ',(cdr view)))))
272 (defvar ampc-item-mode-map
273 (let ((map (make-sparse-keymap)))
274 (suppress-keymap map)
275 (define-key map (kbd "m") 'ampc-mark)
276 (define-key map (kbd "u") 'ampc-unmark)
277 (define-key map (kbd "U") 'ampc-unmark-all)
278 (define-key map (kbd "n") 'ampc-next-line)
279 (define-key map (kbd "p") 'ampc-previous-line)
282 (defvar ampc-current-playlist-mode-map
283 (let ((map (make-sparse-keymap)))
284 (suppress-keymap map)
285 (define-key map (kbd "<return>") 'ampc-play-this)
288 (defvar ampc-playlist-mode-map
289 (let ((map (make-sparse-keymap)))
290 (suppress-keymap map)
291 (define-key map (kbd "t") 'ampc-toggle-marks)
292 (define-key map (kbd "d") 'ampc-delete)
293 (define-key map (kbd "<up>") 'ampc-up)
294 (define-key map (kbd "<down>") 'ampc-down)
297 (defvar ampc-playlists-mode-map
298 (let ((map (make-sparse-keymap)))
299 (suppress-keymap map)
300 (define-key map (kbd "l") 'ampc-load)
301 (define-key map (kbd "r") 'ampc-rename-playlist)
302 (define-key map (kbd "d") 'ampc-delete-playlist)
305 (defvar ampc-tag-song-mode-map
306 (let ((map (make-sparse-keymap)))
307 (suppress-keymap map)
308 (define-key map (kbd "t") 'ampc-toggle-marks)
309 (define-key map (kbd "a") 'ampc-add)
312 (defvar ampc-outputs-mode-map
313 (let ((map (make-sparse-keymap)))
314 (suppress-keymap map)
315 (define-key map (kbd "t") 'ampc-toggle-marks)
316 (define-key map (kbd "a") 'ampc-toggle-output-enabled)
320 (easy-menu-define ampc-menu ampc-mode-map
323 ["Play" ampc-toggle-play
324 :visible (and ampc-status
325 (not (equal (cdr (assoc "state" ampc-status))"play")))]
326 ["Pause" ampc-toggle-play
327 :visible (and ampc-status
328 (equal (cdr (assoc "state" ampc-status)) "play"))]
330 ["Clear playlist" ampc-clear]
331 ["Shuffle playlist" ampc-shuffle]
332 ["Store playlist" ampc-store]
333 ["Queue Playlist" ampc-load :visible (ampc-playlist)]
334 ["Rename Playlist" ampc-rename-playlist :visible (ampc-playlist)]
335 ["Delete Playlist" ampc-delete-playlist :visible (ampc-playlist)]
337 ["Increase volume" ampc-increase-volume]
338 ["Decrease volume" ampc-decrease-volume]
339 ["Increase crossfade" ampc-increase-crossfade]
340 ["Decrease crossfade" ampc-decrease-crossfade]
341 ["Toggle repeat" ampc-toggle-repeat]
342 ["Toggle random" ampc-toggle-random]
343 ["Toggle consume" ampc-toggle-consume]
345 ["Trigger update" ampc-trigger-update]
348 (easy-menu-define ampc-selection-menu ampc-item-mode-map
349 "Selection menu for ampc"
351 ["Add to playlist" ampc-add
352 :visible (not (eq (car ampc-type) 'outputs))]
353 ["Toggle enabled" ampc-toggle-output-enabled
354 :visible (eq (car ampc-type) 'outputs)]
356 ["Next line" ampc-next-line]
357 ["Previous line" ampc-previous-line]
359 ["Unmark" ampc-unmark]
360 ["Unmark all" ampc-unmark-all]
361 ["Toggle marks" ampc-toggle-marks
362 :visible (not (eq (car ampc-type) 'playlists))]))
366 (defmacro ampc-with-buffer (type &rest body)
367 (declare (indent 1) (debug t))
368 `(let* ((type- ,type)
369 (b (loop for b in ampc-buffers
370 when (with-current-buffer b
371 (cond ((windowp type-)
372 (eq (window-buffer type-)
375 (eq (car ampc-type) type-))
377 (equal ampc-type type-))))
381 (with-current-buffer b
382 (let ((buffer-read-only))
383 ,@(if (eq (car body) 'no-se)
386 (goto-char (point-min))
389 (defmacro ampc-fill-skeleton (tag &rest body)
390 (declare (indent 1) (debug t))
392 (data-buffer (current-buffer)))
393 (ampc-with-buffer tag-
395 (let ((point (point)))
396 (goto-char (point-min))
398 do (put-text-property (point) (1+ (point)) 'updated t)
400 (goto-char (point-min))
402 (goto-char (point-min))
404 when (get-text-property (point) 'updated)
405 do (delete-region (point) (1+ (line-end-position)))
407 do (forward-line nil)
412 (with-selected-window (if (windowp tag-) tag- (ampc-get-window tag-))
415 (defmacro ampc-with-selection (arg &rest body)
416 (declare (indent 1) (debug t))
420 (goto-char (point-min))
421 (search-forward-regexp "^* " nil t)))
422 (loop initially (goto-char (point-min))
423 while (search-forward-regexp "^* " nil t)
429 for index from 0 to (1- (prefix-numeric-value arg-))
431 (goto-char (line-end-position))
433 until (ampc-next-line)))))
436 (define-derived-mode ampc-outputs-mode ampc-item-mode "ampc-o"
439 (define-derived-mode ampc-tag-song-mode ampc-item-mode "ampc-ts"
442 (define-derived-mode ampc-current-playlist-mode ampc-playlist-mode "ampc-cpl"
445 (define-derived-mode ampc-playlist-mode ampc-item-mode "ampc-pl"
448 (define-derived-mode ampc-playlists-mode ampc-item-mode "ampc-pls"
451 (define-derived-mode ampc-item-mode ampc-mode ""
454 (define-derived-mode ampc-mode fundamental-mode "ampc"
456 (buffer-disable-undo)
457 (setf buffer-read-only t
458 truncate-lines ampc-truncate-lines
459 font-lock-defaults '((("^\\(\\*\\)\\(.*\\)$"
461 (2 'ampc-marked-face))
462 ("^ .*$" 0 'ampc-face))
465 (define-minor-mode ampc-highlight-current-song-mode ""
469 (funcall (if ampc-highlight-current-song-mode
470 'font-lock-add-keywords
471 'font-lock-remove-keywords)
473 '((ampc-find-current-song
474 (1 'ampc-current-song-mark-face)
475 (2 'ampc-current-song-marked-face)))))
477 ;;; *** internal functions
478 (defun ampc-add-impl (&optional data)
480 (loop for d in (get-text-property (line-end-position) 'data)
481 do (ampc-add-impl d)))
483 (avl-tree-mapc (lambda (e) (ampc-add-impl (cdr e))) data))
486 (ampc-send-command 'playlistadd t (ampc-playlist) data)
487 (ampc-send-command 'add t data)))
490 do (ampc-add-impl (cdr (assoc "file" d)))))))
492 (defun* ampc-skip (N &aux (song (cdr-safe (assoc "song" ampc-status))))
494 (ampc-send-command 'play nil (max 0 (+ (string-to-number song) N)))))
496 (defun* ampc-find-current-song
497 (limit &aux (point (point)) (song (cdr-safe (assoc "song" ampc-status))))
499 (<= (1- (line-number-at-pos (point)))
500 (setf song (string-to-number song)))
501 (>= (1- (line-number-at-pos limit)) song))
502 (goto-char (point-min))
505 (narrow-to-region (max point (point)) (min limit (line-end-position)))
506 (search-forward-regexp "\\(?1:\\(\\`\\*\\)?\\)\\(?2:.*\\)$"))))
508 (defun ampc-set-volume (arg func)
509 (when (or arg ampc-status)
513 (or (and arg (prefix-numeric-value arg))
514 (max (min (funcall func
516 (cdr (assoc "volume" ampc-status)))
521 (defun ampc-set-crossfade (arg func)
522 (when (or arg ampc-status)
526 (or (and arg (prefix-numeric-value arg))
528 (string-to-number (cdr (assoc "xfade" ampc-status)))
532 (defun* ampc-fix-pos (f &aux buffer-read-only)
534 (move-beginning-of-line nil)
535 (let* ((data (get-text-property (+ 2 (point)) 'data))
536 (pos (assoc "Pos" data)))
537 (setf (cdr pos) (funcall f (cdr pos)))
538 (put-text-property (+ 2 (point))
543 (defun* ampc-move-impl (up &aux (line (1- (line-number-at-pos))))
544 (when (or (and up (eq line 0))
545 (and (not up) (eq (1+ line) (line-number-at-pos (1- (point-max))))))
546 (return-from ampc-move-impl t))
548 (move-beginning-of-line nil)
550 (ampc-send-command 'playlistmove
554 (funcall (if up '1- '1+)
556 (ampc-send-command 'move nil line (funcall (if up '1- '1+) line)))
559 (unless (ampc-playlist)
564 (let ((buffer-read-only))
565 (transpose-lines 1)))
571 (defun* ampc-move (up N &aux (point (point)))
572 (goto-char (if up (point-min) (point-max)))
574 (funcall (if up 'search-forward-regexp 'search-backward-regexp)
578 (loop until (ampc-move-impl up)
580 do (search-backward-regexp "^* " nil t)
582 until (not (funcall (if up
583 'search-forward-regexp
584 'search-backward-regexp)
595 (unless (eq (1- N) 0)
596 (setf N (- (- (forward-line (1- N)) (1- N))))))
598 until (ampc-move-impl up)))))
600 (defun ampc-playlist ()
601 (ampc-with-buffer 'playlists
602 (if (search-forward-regexp "^* \\(.*\\)$" nil t)
605 (buffer-substring-no-properties
606 (+ (line-beginning-position) 2)
607 (line-end-position))))))
609 (defun* ampc-mark-impl (select N &aux result buffer-read-only)
610 (when (eq (car ampc-type) 'playlists)
611 (assert (or (not select) (null N) (eq N 1)))
612 (ampc-with-buffer 'playlists
613 (loop while (search-forward-regexp "^\\* " nil t)
614 do (replace-match " " nil nil))))
615 (loop repeat (or N 1)
617 do (move-beginning-of-line nil)
619 (insert (if select "*" " "))
620 (setf result (ampc-next-line nil)))
621 (ampc-post-mark-change-update)
624 (defun ampc-post-mark-change-update ()
625 (ecase (car ampc-type)
626 ((current-playlist playlist outputs))
628 (ampc-update-playlist))
630 (loop for w in (ampc-windows)
633 do (with-current-buffer (window-buffer w)
634 (when (member (car ampc-type) '(song tag))
637 if (eq w (selected-window))
640 (ampc-fill-tag-song))))
642 (defun ampc-pad (alist)
643 (loop for (offset . data) in alist
645 with current-offset = 0
646 when (<= current-offset offset)
647 when (and (not first) (eq (- offset current-offset) 0))
650 and concat (make-string (- offset current-offset) ? )
651 and do (setf current-offset offset)
654 and do (incf current-offset)
657 do (setf current-offset (+ current-offset (length data))
660 (defun ampc-update-header ()
661 (if (eq (car ampc-type) 'status)
662 (setf header-line-format nil)
663 (setf header-line-format
665 (make-string (floor (fringe-columns 'left t)) ? )
666 (ecase (car ampc-type)
668 (concat " " (plist-get (cdr ampc-type) :tag)))
672 (ampc-pad (loop for p in (plist-get (cdr ampc-type) :properties)
673 collect `(,(or (plist-get (cdr p) :offset) 2) .
674 ,(or (plist-get (cdr p) :title)
677 " [ Updating... ]")))))
679 (defun ampc-set-dirty (tag-or-dirty &optional dirty)
680 (if (or (null tag-or-dirty) (eq tag-or-dirty t))
681 (progn (setf ampc-dirty tag-or-dirty)
682 (ampc-update-header))
683 (loop for w in (ampc-windows)
684 do (with-current-buffer (window-buffer w)
685 (when (eq (car ampc-type) tag-or-dirty)
686 (ampc-set-dirty dirty))))))
688 (defun ampc-update ()
689 (loop for b in ampc-buffers
690 do (with-current-buffer b
692 (ecase (car ampc-type)
694 (ampc-send-command 'outputs))
696 (ampc-update-playlist))
700 (ampc-send-command 'listallinfo)))
702 (ampc-send-command 'status)
703 (ampc-send-command 'currentsong))
705 (ampc-send-command 'listplaylists))
707 (ampc-send-command 'playlistinfo)))))))
709 (defun ampc-update-playlist ()
710 (ampc-with-buffer 'playlists
711 (if (search-forward-regexp "^\\* " nil t)
712 (ampc-send-command 'listplaylistinfo
714 (get-text-property (point) 'data))
715 (ampc-with-buffer 'playlist
716 (delete-region (point-min) (point-max))
717 (ampc-set-dirty nil)))))
719 (defun ampc-send-command-impl (command)
721 (message (concat "ampc: " command)))
722 (process-send-string ampc-connection (concat command "\n")))
724 (defun ampc-send-command (command &optional unique &rest args)
725 (if (equal command 'idle)
726 (when ampc-working-timer
727 (cancel-timer ampc-working-timer)
729 ampc-working-timer nil)
731 (unless ampc-working-timer
733 ampc-working-timer (run-at-time nil 0.1 'ampc-yield))))
734 (setf command `(,command ,@args))
735 (when (equal (car-safe ampc-outstanding-commands) '(idle))
736 (setf (car ampc-outstanding-commands) '(noidle))
737 (ampc-send-command-impl "noidle"))
738 (setf ampc-outstanding-commands
740 ampc-outstanding-commands
741 (remove command ampc-outstanding-commands))
744 (defun ampc-send-next-command ()
745 (unless ampc-outstanding-commands
746 (ampc-send-command 'idle))
747 (ampc-send-command-impl (concat (symbol-name (caar ampc-outstanding-commands))
749 (cdar ampc-outstanding-commands)
751 concat (cond ((integerp a)
752 (number-to-string a))
755 (defun ampc-tree< (a b)
756 (not (string< (if (listp a) (car a) a) (if (listp b) (car b) b))))
758 (defun ampc-create-tree ()
759 (avl-tree-create 'ampc-tree<))
761 (defun ampc-extract (tag &optional buffer)
762 (with-current-buffer (or buffer (current-buffer))
764 (ampc-extract (plist-get tag :tag))
766 (goto-char (point-min))
767 (when (search-forward-regexp
768 (concat "^" (regexp-quote tag) ": \\(.*\\)$")
771 (let ((result (match-string 1)))
772 (when (equal tag "Time")
773 (setf result (ampc-transform-time result)))
776 (defun ampc-insert (element data &optional cmp)
778 (goto-char (point-min))
781 for tp = (get-text-property (+ (point) 2) 'data)
782 finally return 'insert
785 (let ((s (buffer-substring-no-properties
787 (line-end-position))))
788 (cond ((equal s element)
789 (unless (member data tp)
790 (put-text-property (+ (point) 2)
791 (1+ (line-end-position))
798 (let ((r (funcall cmp data tp)))
799 (if (memq r '(update insert))
801 (forward-line (1- r))
806 (let ((s (buffer-substring-no-properties
808 (line-end-position))))
809 (unless (string< s element)
814 (let ((start (point)))
815 (insert element "\n")
816 (put-text-property start (point) 'data (if (eq cmp t)
821 (remove-text-properties (point) (1+ (point)) '(updated))
822 (equal (buffer-substring (point) (1+ (point))) "*")))))
824 (defun ampc-fill-tag (trees)
825 (put-text-property (point-min) (point-max) 'data nil)
827 finally return new-trees
829 do (avl-tree-mapc (lambda (e)
830 (when (ampc-insert (car e) (cdr e) t)
831 (push (cdr e) new-trees)))
834 (defun ampc-fill-song (trees)
837 do (loop for song in songs
840 (loop for (p . v) in (plist-get (cdr ampc-type) :properties)
841 collect `(,(- (or (plist-get v :offset) 2) 2)
842 . ,(or (cdr-safe (assoc p song)) ""))))
845 (defun* ampc-narrow-entry (&optional (delimiter "file"))
846 (narrow-to-region (move-beginning-of-line nil)
847 (or (progn (goto-char (line-end-position))
848 (when (search-forward-regexp
849 (concat "^" (regexp-quote delimiter) ": ")
852 (move-beginning-of-line nil)
856 (defun ampc-get-window (type)
857 (loop for w in (ampc-windows)
858 thereis (with-current-buffer (window-buffer w)
859 (when (eq (car ampc-type) type)
862 (defun* ampc-fill-playlist (&aux properties)
863 (ampc-fill-skeleton 'playlist
864 (setf properties (plist-get (cdr ampc-type) :properties))
865 (with-current-buffer data-buffer
868 while (search-forward-regexp "^file: " nil t)
871 (let ((file (ampc-extract "file"))
874 (loop for (tag . tag-properties) in properties
875 collect `(,(- (or (plist-get tag-properties
879 . ,(ampc-extract tag))))))
880 (ampc-with-buffer 'playlist
885 (let ((p1 (cdr (assoc 'index a)))
886 (p2 (cdr (assoc 'index b))))
887 (cond ((< p1 p2) 'update)
889 (if (equal (cdr (assoc "file" a))
890 (cdr (assoc "file" b)))
893 (t (- p1 p2)))))))))))))
895 (defun* ampc-fill-outputs (&aux properties)
896 (ampc-fill-skeleton 'outputs
897 (setf properties (plist-get (cdr ampc-type) :properties))
898 (with-current-buffer data-buffer
900 while (search-forward-regexp "^outputid: " nil t)
902 (ampc-narrow-entry "outputid")
903 (let ((outputid (ampc-extract "outputid"))
904 (outputenabled (ampc-extract "outputenabled"))
907 (loop for (tag . tag-properties) in properties
908 collect `(,(- (or (plist-get tag-properties :offset)
911 . ,(ampc-extract tag))))))
912 (ampc-with-buffer 'outputs
913 (ampc-insert text `(("outputid" . ,outputid)
914 ("outputenabled" . ,outputenabled))))))))))
916 (defun* ampc-fill-current-playlist (&aux properties)
917 (ampc-fill-skeleton 'current-playlist
918 (setf properties (plist-get (cdr ampc-type) :properties))
919 (with-current-buffer data-buffer
921 while (search-forward-regexp "^file: " nil t)
924 (let ((file (ampc-extract "file"))
925 (pos (ampc-extract "Pos"))
928 (loop for (tag . tag-properties) in properties
929 collect `(,(- (or (plist-get tag-properties :offset)
932 . ,(ampc-extract tag))))))
933 (ampc-with-buffer 'current-playlist
936 ("Pos" . ,(string-to-number pos)))
938 (let ((p1 (cdr (assoc "Pos" a)))
939 (p2 (cdr (assoc "Pos" b))))
940 (cond ((< p1 p2) 'insert)
942 (if (equal (cdr (assoc "file" a))
943 (cdr (assoc "file" b)))
946 (t (- p1 p2)))))))))))))
948 (defun ampc-fill-playlists ()
949 (ampc-fill-skeleton 'playlists
950 (with-current-buffer data-buffer
951 (loop while (search-forward-regexp "^playlist: \\(.*\\)$" nil t)
952 for playlist = (match-string 1)
953 do (ampc-with-buffer 'playlists
954 (ampc-insert playlist playlist))))))
957 (setf ampc-yield (1+ ampc-yield))
960 (defun ampc-fill-status ()
961 (ampc-with-buffer 'status
962 (delete-region (point-min) (point-max))
963 (funcall (or (plist-get (cadr ampc-type) :filler)
964 'ampc-fill-status-default))
965 (ampc-set-dirty nil)))
967 (defun ampc-fill-status-default ()
968 (let ((flags (mapconcat
970 (loop for (f . n) in '(("repeat" . "Repeat")
971 ("random" . "Random")
972 ("consume" . "Consume"))
973 when (equal (cdr (assoc f ampc-status)) "1")
977 (state (cdr (assoc "state" ampc-status))))
978 (insert (concat "State: " state
980 (concat (make-string (- 10 (length state)) ? )
981 (ecase (% ampc-yield 4)
987 (when (equal state "play")
989 (cdr (assoc "Artist" ampc-status))
991 (cdr (assoc "Title" ampc-status))
993 "Volume: " (cdr (assoc "volume" ampc-status)) "\n"
994 "Crossfade: " (cdr (assoc "xfade" ampc-status)) "\n"
995 (unless (equal flags "")
996 (concat flags "\n"))))))
998 (defun ampc-fill-tag-song ()
1000 with trees = `(,ampc-internal-db)
1001 for w in (ampc-windows)
1004 (when (member (car ampc-type) '(tag song))
1006 (ampc-fill-skeleton w
1007 (ecase (car ampc-type)
1008 (tag (setf trees (ampc-fill-tag trees)))
1009 (song (ampc-fill-song trees))))
1011 (loop while (search-forward-regexp "^* " nil t)
1012 do (setf trees (append (get-text-property (point) 'data)
1015 (defun* ampc-transform-time (data &aux (time (string-to-number data)))
1016 (concat (number-to-string (/ time 60))
1018 (when (< (% time 60) 10)
1020 (number-to-string (% time 60))))
1022 (defun ampc-handle-idle ()
1024 for subsystem = (buffer-substring (point) (line-end-position))
1025 when (string-match "^changed: \\(.*\\)$" subsystem)
1026 do (case (intern (match-string 1 subsystem))
1028 (setf ampc-internal-db nil)
1029 (ampc-set-dirty 'tag t)
1030 (ampc-set-dirty 'song t))
1032 (ampc-set-dirty 'outputs t))
1033 ((player options mixer)
1034 (setf ampc-status nil)
1035 (ampc-set-dirty 'status t))
1037 (ampc-set-dirty 'playlists t)
1038 (ampc-set-dirty 'playlist t))
1040 (ampc-set-dirty 'current-playlist t)
1041 (ampc-set-dirty 'status t)))
1046 (defun ampc-handle-setup (status)
1047 (unless (and (string-match "^ MPD \\(.+\\)\\.\\(.+\\)\\.\\(.+\\)$"
1049 (let ((version-a (string-to-number (match-string 1 status)))
1050 (version-b (string-to-number (match-string 2 status)))
1051 ;; (version-c (string-to-number (match-string 2 status)))
1054 (>= version-b 15))))
1055 (error (concat "Your version of MPD is not supported. "
1056 "ampc supports MPD 0.15.0 and later"))))
1058 (defun ampc-fill-internal-db ()
1059 (setf ampc-internal-db (ampc-create-tree))
1060 (loop while (search-forward-regexp "^file: " nil t)
1061 do (save-restriction
1063 (ampc-fill-internal-db-entry)))
1064 (ampc-fill-tag-song))
1066 (defun ampc-fill-internal-db-entry ()
1068 with data-buffer = (current-buffer)
1069 with tree = `(nil . ,ampc-internal-db)
1070 for w in (ampc-windows)
1072 (with-current-buffer (window-buffer w)
1074 (ecase (car ampc-type)
1076 (let* ((data (ampc-extract (cdr ampc-type) data-buffer))
1077 (member (and (cdr tree) (avl-tree-member (cdr tree) data))))
1079 (cond (member (setf tree member))
1081 (setf member `(,data . nil))
1082 (avl-tree-enter (cdr tree) member)
1085 (setf (cdr tree) (ampc-create-tree) member`(,data . nil))
1086 (avl-tree-enter (cdr tree) member)
1087 (setf tree member)))))
1089 (push (loop for p in `(("file")
1090 ,@(plist-get (cdr ampc-type) :properties))
1091 for data = (ampc-extract (car p) data-buffer)
1093 collect `(,(car p) . ,data)
1098 (defun ampc-handle-current-song ()
1099 (loop for k in '("Artist" "Title")
1100 for s = (ampc-extract k)
1102 do (push `(,k . ,s) ampc-status)
1106 (defun ampc-handle-status ()
1107 (loop for k in '("volume" "repeat" "random" "consume" "xfade" "state" "song")
1108 for v = (ampc-extract k)
1110 do (push `(,k . ,v) ampc-status)
1112 (ampc-with-buffer 'current-playlist
1113 (when ampc-highlight-current-song-mode
1114 (font-lock-fontify-region (point-min) (point-max)))))
1116 (defun ampc-handle-update ()
1117 (message "Database update started"))
1119 (defun ampc-handle-command (status)
1120 (if (eq status 'error)
1121 (pop ampc-outstanding-commands)
1122 (case (car (pop ampc-outstanding-commands))
1126 (ampc-handle-setup status))
1128 (ampc-handle-current-song))
1130 (ampc-handle-status))
1132 (ampc-handle-update))
1134 (ampc-fill-playlist))
1136 (ampc-fill-playlists))
1138 (ampc-fill-current-playlist))
1140 (ampc-fill-internal-db))
1142 (ampc-fill-outputs))))
1143 (unless ampc-outstanding-commands
1145 (ampc-send-next-command))
1147 (defun ampc-filter (_process string)
1148 (assert (buffer-live-p (process-buffer ampc-connection)))
1149 (with-current-buffer (process-buffer ampc-connection)
1152 (message "ampc: -> %s" string))
1153 (goto-char (process-mark ampc-connection))
1155 (set-marker (process-mark ampc-connection) (point)))
1157 (goto-char (point-min))
1159 (when (or (and (search-forward-regexp
1160 "^ACK \\[\\(.*\\)\\] {.*} \\(.*\\)\n\\'"
1163 (message "ampc command error: %s (%s)"
1167 (and (search-forward-regexp "^OK\\(.*\\)\n\\'" nil t)
1169 (let ((match-end (match-end 0)))
1171 (narrow-to-region (point-min) match-end)
1172 (goto-char (point-min))
1173 (ampc-handle-command (if success (match-string 1) 'error)))
1174 (delete-region (point-min) match-end)))))))
1176 ;;; **** window management
1177 (defun ampc-windows (&optional unordered)
1178 (loop for f being the frame
1179 thereis (loop for w being the windows of f
1180 when (eq (window-buffer w) (car ampc-buffers))
1181 return (loop for b in (if unordered
1182 ampc-buffers-unordered
1185 (loop for w being the windows of f
1186 thereis (and (eq (window-buffer w)
1190 (defun* ampc-configure-frame-1 (split &aux (split-type (car split)))
1191 (if (member split-type '(vertical horizontal))
1193 (loop with length = (if (eq split-type 'horizontal) (window-width)
1197 for subsplit in (cdr split)
1198 for s = (car subsplit)
1201 and do (setf rest-car sizes)
1203 do (let ((l (if (integerp s) s (floor (* s length)))))
1204 (setf rest (- rest l))
1206 finally do (setf (car rest-car) rest))
1207 (let ((first-window (selected-window)))
1208 (setf sizes (nreverse sizes))
1209 (loop for size in (loop for s in sizes
1211 for window on (cdr sizes)
1216 (eq split-type 'horizontal)))))
1217 (setf (car sizes) first-window))
1218 (loop for subsplit in (cdr split)
1220 do (with-selected-window window
1221 (ampc-configure-frame-1 (cdr subsplit)))
1222 if (plist-get (cddr subsplit) :point)
1223 do (select-window window)
1225 (setf (window-dedicated-p (selected-window)) nil)
1228 (pop-to-buffer-same-window
1229 (get-buffer-create (concat "*ampc "
1230 (or (plist-get (cdr split) :tag) "Song")
1232 (ampc-tag-song-mode))
1234 (pop-to-buffer-same-window (get-buffer-create "*ampc Outputs*"))
1235 (ampc-outputs-mode))
1237 (pop-to-buffer-same-window (get-buffer-create "*ampc Current Playlist*"))
1238 (ampc-current-playlist-mode)
1239 (ampc-highlight-current-song-mode 1))
1241 (pop-to-buffer-same-window (get-buffer-create "*ampc Playlist*"))
1242 (ampc-playlist-mode))
1244 (pop-to-buffer-same-window (get-buffer-create "*ampc Playlists*"))
1245 (ampc-playlists-mode))
1247 (pop-to-buffer-same-window (get-buffer-create "*ampc Status*"))
1249 (destructuring-bind (&key (dedicated t) (mode-line t) &allow-other-keys)
1251 (setf (window-dedicated-p (selected-window)) dedicated)
1253 (setf mode-line-format nil)))
1254 (setf ampc-type split)
1255 (add-to-list 'ampc-all-buffers (current-buffer))
1256 (push `(,(or (plist-get (cdr split) :id)
1257 (if (eq (car ampc-type) 'song) 9998 9999))
1258 . ,(current-buffer))
1260 (ampc-set-dirty t)))
1262 (defun ampc-configure-frame (split)
1263 (if ampc-use-full-frame
1264 (progn (setf (window-dedicated-p (selected-window)) nil)
1265 (delete-other-windows))
1266 (loop with live-window = nil
1267 for w in (nreverse (ampc-windows t))
1268 if (window-live-p w)
1269 if (not live-window)
1270 do (setf live-window w)
1272 do (delete-window w)
1275 finally do (if live-window (select-window live-window))))
1276 (setf ampc-buffers nil)
1277 (ampc-configure-frame-1 split)
1278 (setf ampc-buffers-unordered (mapcar 'cdr ampc-buffers)
1279 ampc-buffers (mapcar 'cdr (sort ampc-buffers
1280 (lambda (a b) (< (car a) (car b))))))
1283 ;;; *** interactives
1284 (defun* ampc-unmark-all (&aux buffer-read-only)
1288 (goto-char (point-min))
1289 (loop while (search-forward-regexp "^\\* " nil t)
1290 do (replace-match " " nil nil)))
1291 (ampc-post-mark-change-update))
1293 (defun ampc-trigger-update ()
1294 "Trigger a database update."
1296 (ampc-send-command 'update))
1298 (defun* ampc-toggle-marks (&aux buffer-read-only)
1299 "Toggle marks. Marked entries become unmarked, and vice versa."
1302 (loop for (a . b) in '(("* " . "T ")
1305 do (goto-char (point-min))
1306 (loop while (search-forward-regexp (concat "^" (regexp-quote a))
1309 do (replace-match b nil nil))))
1310 (ampc-post-mark-change-update))
1312 (defun ampc-up (&optional arg)
1313 "Go to the previous ARG'th entry.
1314 With optional prefix ARG, move the next ARG entries after point
1315 rather than the selection."
1319 (defun ampc-down (&optional arg)
1320 "Go to the next ARG'th entry.
1321 With optional prefix ARG, move the next ARG entries after point
1322 rather than the selection."
1324 (ampc-move nil arg))
1326 (defun ampc-mark (&optional arg)
1327 "Mark the next ARG'th entries.
1330 (ampc-mark-impl t arg))
1332 (defun ampc-unmark (&optional arg)
1333 "Unmark the next ARG'th entries.
1336 (ampc-mark-impl nil arg))
1338 (defun ampc-increase-volume (&optional arg)
1340 With prefix argument ARG, set volume to ARG percent."
1342 (ampc-set-volume arg '+))
1344 (defun ampc-decrease-volume (&optional arg)
1346 With prefix argument ARG, set volume to ARG percent."
1348 (ampc-set-volume arg '-))
1350 (defun ampc-increase-crossfade (&optional arg)
1351 "Increase crossfade.
1352 With prefix argument ARG, set crossfading to ARG seconds."
1354 (ampc-set-crossfade arg '+))
1356 (defun ampc-decrease-crossfade (&optional arg)
1357 "Decrease crossfade.
1358 With prefix argument ARG, set crossfading to ARG seconds."
1360 (ampc-set-crossfade arg '-))
1362 (defun ampc-toggle-repeat (&optional arg)
1363 "Toggle MPD's repeat state.
1364 With prefix argument ARG, enable repeating if ARG is positive,
1365 otherwise disable it."
1367 (when (or arg ampc-status)
1368 (ampc-send-command 'repeat
1371 (if (equal (cdr (assoc "repeat" ampc-status)) "1")
1374 ((> (prefix-numeric-value arg) 0) 1)
1377 (defun ampc-toggle-consume (&optional arg)
1378 "Toggle MPD's consume state.
1379 With prefix argument ARG, enable consuming if ARG is positive,
1380 otherwise disable it.
1382 When consume is activated, each song played is removed from the playlist."
1384 (when (or arg ampc-status)
1385 (ampc-send-command 'consume
1389 (assoc "consume" ampc-status))
1393 ((> (prefix-numeric-value arg) 0) 1)
1396 (defun ampc-toggle-random (&optional arg)
1397 "Toggle MPD's random state.
1398 With prefix argument ARG, enable random playing if ARG is positive,
1399 otherwise disable it."
1401 (when (or arg ampc-status)
1402 (ampc-send-command 'random
1405 (if (equal (cdr (assoc "random" ampc-status)) "1")
1408 ((> (prefix-numeric-value arg) 0) 1)
1411 (defun ampc-play-this ()
1412 "Play selected song."
1415 (ampc-send-command 'play nil (1- (line-number-at-pos)))
1416 (ampc-send-command 'pause nil 0)))
1418 (defun ampc-toggle-play (&optional arg)
1420 If mpd does not play a song already, start playing the song at
1421 point if the current buffer is the playlist buffer, otherwise
1422 start at the beginning of the playlist.
1424 If ARG is 4, stop player rather than pause if applicable."
1427 (setf arg (prefix-numeric-value arg)))
1428 (case (intern (cdr (assoc "state" ampc-status)))
1430 (when (or (null arg) (> arg 0))
1434 (if (and (eq (car ampc-type) 'current-playlist) (not (eobp)))
1435 (1- (line-number-at-pos))
1438 (when (or (null arg) (> arg 0))
1439 (ampc-send-command 'pause nil 0)))
1441 (cond ((or (null arg) (< arg 0))
1442 (ampc-send-command 'pause nil 1))
1444 (ampc-send-command 'stop))))))
1446 (defun ampc-next (&optional arg)
1448 With prefix argument ARG, skip ARG songs."
1450 (ampc-skip (or arg 1)))
1452 (defun ampc-previous (&optional arg)
1453 "Play previous song.
1454 With prefix argument ARG, skip ARG songs."
1456 (ampc-skip (- (or arg 1))))
1458 (defun ampc-rename-playlist (new-name)
1459 "Rename selected playlist to NEW-NAME.
1460 Interactively, read NEW-NAME from the minibuffer."
1461 (interactive "MNew name: ")
1463 (ampc-send-command 'rename nil (ampc-playlist) new-name)
1464 (error "No playlist selected")))
1467 "Load selected playlist in the current playlist."
1470 (ampc-send-command 'load nil (ampc-playlist))
1471 (error "No playlist selected")))
1473 (defun ampc-toggle-output-enabled (&optional arg)
1474 "Toggle the next ARG outputs.
1475 If ARG is omitted, use the selected entries."
1477 (ampc-with-selection arg
1478 (let ((data (get-text-property (point) 'data)))
1479 (ampc-send-command (if (equal (cdr (assoc "outputenabled" data)) "1")
1483 (cdr (assoc "outputid" data))))))
1485 (defun ampc-delete (&optional arg)
1486 "Delete the next ARG songs from the playlist.
1487 If ARG is omitted, use the selected entries."
1489 (let ((point (point)))
1490 (ampc-with-selection arg
1491 (let ((val (1- (- (line-number-at-pos) index))))
1493 (ampc-send-command 'playlistdelete t (ampc-playlist) val)
1494 (ampc-send-command 'delete t val))))
1496 (ampc-align-point)))
1498 (defun ampc-align-point ()
1500 (move-beginning-of-line nil)
1503 (defun ampc-shuffle ()
1506 (if (not (ampc-playlist))
1507 (ampc-send-command 'shuffle)
1508 (ampc-with-buffer 'playlist
1512 (sort (loop until (eobp)
1513 collect `(,(cdr (assoc "file" (get-text-property
1519 (< (cdr a) (cdr b)))))))
1521 (loop for s in shuffled
1522 do (ampc-add-impl s))))))
1524 (defun ampc-clear ()
1528 (ampc-send-command 'playlistclear nil (ampc-playlist))
1529 (ampc-send-command 'clear)))
1531 (defun* ampc-add (&optional arg)
1532 "Add the next ARG songs associated with the entries after point
1534 If ARG is omitted, use the selected entries in the current buffer."
1536 (ampc-with-selection arg
1539 (defun ampc-delete-playlist ()
1540 "Delete selected playlist."
1542 (ampc-with-selection nil
1543 (let ((name (get-text-property (point) 'data)))
1544 (when (y-or-n-p (concat "Delete playlist " name "?"))
1545 (ampc-send-command 'rm nil name)))))
1547 (defun ampc-store (name)
1548 "Store current playlist as NAME.
1549 Interactively, read NAME from the minibuffer."
1550 (interactive "MSave playlist as: ")
1551 (ampc-send-command 'save nil name))
1553 (defun ampc-previous-line (&optional arg)
1554 "Go to previous ARG'th entry in the current buffer.
1557 (ampc-next-line (* (or arg 1) -1)))
1559 (defun ampc-next-line (&optional arg)
1560 "Go to next ARG'th entry in the current buffer.
1565 (progn (forward-line -1)
1572 (defun ampc-quit (&optional arg)
1574 If called with a prefix argument ARG, kill the mpd instance that
1575 ampc is connected to."
1577 (when (and ampc-connection (member (process-status ampc-connection)
1579 (set-process-filter ampc-connection nil)
1580 (when (equal (car-safe ampc-outstanding-commands) '(idle))
1581 (ampc-send-command-impl "noidle")
1582 (with-current-buffer (process-buffer ampc-connection)
1583 (loop do (goto-char (point-min))
1584 until (search-forward-regexp "^\\(ACK\\)\\|\\(OK\\).*\n\\'" nil t)
1585 do (accept-process-output ampc-connection nil 50))))
1586 (ampc-send-command-impl (if arg "kill" "close")))
1587 (when ampc-working-timer
1588 (cancel-timer ampc-working-timer))
1589 (loop with found-window
1590 for w in (nreverse (ampc-windows t))
1591 when (window-live-p w)
1593 do (delete-window w)
1595 do (setf found-window t)
1596 (setf (window-dedicated-p w) nil)
1599 (loop for b in ampc-all-buffers
1600 when (buffer-live-p b)
1603 (setf ampc-connection nil
1605 ampc-all-buffers nil
1606 ampc-internal-db nil
1607 ampc-working-timer nil
1608 ampc-outstanding-commands nil
1610 (run-hooks 'ampc-quit-hook))
1613 (defun ampc (&optional host port)
1614 "ampc is an asynchronous client for the MPD media player.
1615 This function is the main entry point for ampc.
1617 Non-interactively, HOST and PORT specify the MPD instance to
1618 connect to. The values default to localhost:6600."
1619 (interactive "MHost (localhost): \nMPort (6600): ")
1620 (when ampc-connection
1622 (run-hooks 'ampc-before-startup-hook)
1623 (when (equal host "")
1625 (when (equal port "")
1627 (let ((connection (open-network-stream "ampc"
1628 (with-current-buffer
1629 (get-buffer-create " *mpc*")
1630 (delete-region (point-min)
1633 (or host "localhost")
1635 :type 'plain :return-list t)))
1636 (unless (car connection)
1637 (error "Failed connecting to server: %s"
1638 (plist-get ampc-connection :error)))
1639 (setf ampc-connection (car connection)))
1640 (setf ampc-outstanding-commands '((setup)))
1641 (set-process-coding-system ampc-connection 'utf-8-unix 'utf-8-unix)
1642 (set-process-filter ampc-connection 'ampc-filter)
1643 (set-process-query-on-exit-flag ampc-connection nil)
1644 (ampc-configure-frame (cdar ampc-views))
1645 (run-hooks 'ampc-connected-hook)
1646 (ampc-filter (process-buffer ampc-connection) nil))
1651 ;; eval: (outline-minor-mode 1);
1652 ;; outline-regexp: ";;; \\*+";
1653 ;; lexical-binding: t
1655 ;; indent-tabs-mode: nil