1 ;;; ampc.el --- Asynchronous Music Player Controller
3 ;; Copyright (C) 2011-2012 Free Software Foundation, Inc.
5 ;; Author: Christopher Schmidt <christopher@ch.ristopher.com>
6 ;; Maintainer: Christopher Schmidt <christopher@ch.ristopher.com>
9 ;; Keywords: ampc, mpc, mpd
10 ;; Compatibility: GNU Emacs: 24.x
12 ;; This file is part of GNU Emacs.
14 ;; This program is free software; you can redistribute it and/or modify
15 ;; it under the terms of the GNU General Public License as published by
16 ;; the Free Software Foundation, either version 3 of the License, or
17 ;; (at your option) any later version.
19 ;; This program is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 ;; GNU General Public License for more details.
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
29 ;; ampc is a controller for the Music Player Daemon (http://mpd.wikia.com/).
32 ;; If you use GNU ELPA, install ampc via M-x package-list-packages RET or
33 ;; (package-install 'ampc). Otherwise, grab this file and put it somewhere in
34 ;; your load-path or add the directory the file is in to it, e.g.:
36 ;; (add-to-list 'load-path "~/.emacs.d/ampc")
38 ;; Then add one autoload definition:
40 ;; (autoload 'ampc "ampc" nil t)
42 ;; Optionally bind a key to this function, e.g.:
44 ;; (global-set-key (kbd "<f9>") 'ampc)
46 ;; Byte-compile ampc (M-x byte-compile-file RET /path/to/ampc.el RET) to improve
50 ;; To invoke ampc, call the command `ampc', e.g. via M-x ampc RET. Once ampc is
51 ;; connected to the daemon, it creates its window configuration in the selected
52 ;; window. To make ampc use the full frame rather than the selected window,
53 ;; customise `ampc-use-full-frame'.
55 ;; ampc offers three independent views which expose different parts of the user
56 ;; interface. The current playlist view, the default view at startup, may be
57 ;; accessed using the `J' (that is `S-j') key. The playlist view may be
58 ;; accessed using the `K' key. The outputs view may be accessed using the `L'
61 ;;; *** current playlist view
62 ;; The playlist view should look like this
64 ;; .........................
73 ;; .........................
75 ;; Window one exposes basic information about the daemon, such as the current
76 ;; state (stop/play/pause), the song currently playing, or the volume.
78 ;; All windows, except the status window, contain a tabular list of items. Each
79 ;; item may be selected/marked. There may be multiple selections.
81 ;; To mark an entry, move the point to the entry and press `m' (ampc-mark). To
82 ;; unmark an entry, press `u' (ampc-unmark). To unmark all entries, press `U'
83 ;; (ampc-unmark-all). To toggle marks, press `t' (ampc-toggle-marks). To
84 ;; navigate to the next entry, press `n' (ampc-next-line). Analogous, pressing
85 ;; `p' (ampc-previous-line) moves the point to the previous entry.
87 ;; Window two shows the current playlist. The song that is currently played by
88 ;; the daemon, if any, is highlighted. To delete the selected songs from the
89 ;; playlist, press `d' (ampc-delete). To move the selected songs up, press
90 ;; `<up>' (ampc-up). Analogous, press `<down>' (ampc-down) to move the selected
93 ;; Windows three to five are tag browsers. You use them to narrow the song
94 ;; database to certain songs. Think of tag browsers as filters, analogous to
95 ;; piping `grep' outputs through additional `grep' filters. The property of the
96 ;; songs that is filtered is displayed in the header line of the window.
98 ;; Window six shows the songs that match the filters defined by windows three to
99 ;; five. To add the selected song to the playlist, press `a' (ampc-add). This
100 ;; key binding works in tag browsers as well. Calling ampc-add in a tag browser
101 ;; adds all songs filtered up to the selected browser to the playlist.
103 ;; The tag browsers of the (default) current playlist view (accessed via `J')
104 ;; are `Genre' (window 3), `Artist' (window 4) and `Album' (window 5). The key
105 ;; `M' may be used to fire up a slightly modified current playlist view. There
106 ;; is no difference to the default current playlist view other than that the tag
107 ;; browsers filter to `Genre' (window 3), `Album' (window 4) and `Artist'
108 ;; (window 5). Metaphorically speaking, the order of the `grep' filters defined
109 ;; by the tag browsers is different.
111 ;;; *** playlist view
112 ;; The playlist view resembles the current playlist view. The window, which
113 ;; exposes the playlist content, is split, though. The bottom half shows a list
114 ;; of stored playlists. The upper half does not expose the current playlist
115 ;; anymore. Instead, the content of the selected (stored) playlist is shown.
116 ;; All commands that used to work in the current playlist view and modify the
117 ;; current playlist now modify the selected (stored) playlist. The list of
118 ;; stored playlists is the only view in ampc that may have only one marked
121 ;; Again, the key `<' may be used to setup a playlist view with a different
122 ;; order of tag browsers.
125 ;; The outputs view contains a single list which shows the configured outputs of
126 ;; mpd. To toggle the enabled property of the selected outputs, press `a'
127 ;; (ampc-toggle-output-enabled).
130 ;; Aside from `J', `M', `K', `<' and `L', which may be used to select different
131 ;; views, ampc defines the following global keys, which may be used in every
132 ;; window associated with ampc:
134 ;; `k' (ampc-toggle-play): Toggle play state. If mpd does not play a song
135 ;; already, start playing the song at point if the current buffer is the
136 ;; playlist buffer, otherwise start at the beginning of the playlist. With
137 ;; prefix argument 4, stop player rather than pause if applicable.
139 ;; `l' (ampc-next): Play next song.
140 ;; `j' (ampc-previous): Play previous song
142 ;; `c' (ampc-clear): Clear playlist.
143 ;; `s' (ampc-shuffle): Shuffle playlist.
145 ;; `S' (ampc-store): Store playlist.
146 ;; `O' (ampc-load): Load selected playlist in the current playlist.
147 ;; `R' (ampc-rename-playlist): Rename selected playlist.
148 ;; `D' (ampc-delete-playlist): Delete selected playlist.
150 ;; `y' (ampc-increase-volume): Increase volume.
151 ;; `M-y' (ampc-decrease-volume): Decrease volume.
152 ;; `h' (ampc-increase-crossfade): Increase crossfade.
153 ;; `M-h' (ampc-decrease-crossfade): Decrease crossfade.
155 ;; `e' (ampc-toggle-repeat): Toggle repeat state.
156 ;; `r' (ampc-toggle-random): Toggle random state.
157 ;; `f' (ampc-toggle-consume): Toggle consume state.
159 ;; `P' (ampc-goto-current-song): Select the current playlist window and move
160 ;; point to the current song.
162 ;; `T' (ampc-trigger-update): Trigger a database update.
163 ;; `Z' (ampc-suspend): Suspend ampc.
164 ;; `q' (ampc-quit): Quit ampc.
166 ;; The keymap of ampc is designed to fit the QWERTY United States keyboard
167 ;; layout. If you use another keyboard layout, feel free to modify
168 ;; ampc-mode-map. For example, I use a regular QWERTZ German keyboard (layout),
169 ;; so I modify `ampc-mode-map' in my init.el like this:
171 ;; (eval-after-load 'ampc
172 ;; '(flet ((substitute-ampc-key
174 ;; (define-key ampc-mode-map to (lookup-key ampc-mode-map from))
175 ;; (define-key ampc-mode-map from nil)))
176 ;; (substitute-ampc-key (kbd "z") (kbd "Z"))
177 ;; (substitute-ampc-key (kbd "y") (kbd "z"))
178 ;; (substitute-ampc-key (kbd "M-y") (kbd "M-z"))
179 ;; (substitute-ampc-key (kbd "<") (kbd ";"))))
181 ;; If ampc is suspended, you can still use every interactive command that does
182 ;; not directly operate on or with the user interace of ampc. For example it is
183 ;; perfectly fine to call `ampc-increase-volume' or `ampc-toggle-play' via M-x
184 ;; RET. To display the information that is displayed by the status window of
185 ;; ampc, call `ampc-status'.
192 (require 'network-stream)
198 "Asynchronous client for the Music Player Daemon."
201 :group 'applications)
204 (defcustom ampc-debug nil
205 "Non-nil means log communication between ampc and MPD."
208 (defcustom ampc-use-full-frame nil
209 "If non-nil, ampc will use the entire Emacs screen."
212 (defcustom ampc-truncate-lines t
213 "If non-nil, truncate lines in ampc buffers."
216 (defcustom ampc-status-tags nil
217 "List of additional tags of the current song that are added to
218 the internal status of ampc and thus are passed to the functions
219 in `ampc-status-changed-hook'. Each element may be a string that
220 specifies a tag that is returned by MPD's `currentsong'
224 (defcustom ampc-before-startup-hook nil
225 "A hook run before startup.
226 This hook is called as the first thing when ampc is started."
229 (defcustom ampc-connected-hook nil
230 "A hook run after ampc connected to MPD."
233 (defcustom ampc-suspend-hook nil
234 "A hook run when suspending ampc."
237 (defcustom ampc-quit-hook nil
238 "A hook run when exiting ampc."
241 (defcustom ampc-status-changed-hook nil
242 "A hook run whenever the status of the daemon (that is volatile
243 properties such as volume or current song) changes. The hook is
244 run with one arg, an alist that contains the new status. The car
245 of each entry is a symbol, the cdr is a string. Valid keys are:
257 and the keys in `ampc-status-tags'. Not all keys may be present
262 (defface ampc-mark-face '((t (:inherit font-lock-constant-face)))
264 (defface ampc-marked-face '((t (:inherit warning)))
265 "Face of marked entries.")
266 (defface ampc-face '((t (:inerhit default)))
267 "Face of unmarked entries.")
268 (defface ampc-current-song-mark-face '((t (:inherit region)))
269 "Face of mark of the current song.")
270 (defface ampc-current-song-marked-face '((t (:inherit region)))
271 "Face of the current song if marked.")
273 ;;; *** internal variables
275 (let* ((songs '(1.0 song :properties (("Track" :title "#")
277 ("Time" :offset 26))))
280 (0.33 tag :tag "Genre" :id 1)
281 (0.33 tag :tag "Artist" :id 2)
282 (1.0 tag :tag "Album" :id 3))
286 (0.33 tag :tag "Genre" :id 1)
287 (0.33 tag :tag "Album" :id 2)
288 (1.0 tag :tag "Artist" :id 3))
291 ("Artist" :offset 20)
293 ("Time" :offset 60))))
298 (1.0 current-playlist :properties ,pl-prop))
304 (1.0 current-playlist :properties ,pl-prop))
311 (0.8 playlist :properties ,pl-prop)
319 (0.8 playlist :properties ,pl-prop)
323 outputs :properties (("outputname" :title "Name")
324 ("outputenabled" :title "Enabled" :offset 10))))))
326 (defvar ampc-connection nil)
327 (defvar ampc-host nil)
328 (defvar ampc-port nil)
329 (defvar ampc-outstanding-commands nil)
331 (defvar ampc-working-timer nil)
332 (defvar ampc-yield nil)
334 (defvar ampc-buffers nil)
335 (defvar ampc-buffers-unordered nil)
336 (defvar ampc-all-buffers nil)
338 (defvar ampc-type nil)
339 (make-variable-buffer-local 'ampc-type)
340 (defvar ampc-dirty nil)
341 (make-variable-buffer-local 'ampc-dirty)
343 (defvar ampc-internal-db nil)
344 (defvar ampc-status nil)
347 (defvar ampc-mode-map
348 (let ((map (make-sparse-keymap)))
349 (suppress-keymap map)
350 (define-key map (kbd "k") 'ampc-toggle-play)
351 (define-key map (kbd "l") 'ampc-next)
352 (define-key map (kbd "j") 'ampc-previous)
353 (define-key map (kbd "c") 'ampc-clear)
354 (define-key map (kbd "s") 'ampc-shuffle)
355 (define-key map (kbd "S") 'ampc-store)
356 (define-key map (kbd "O") 'ampc-load)
357 (define-key map (kbd "R") 'ampc-rename-playlist)
358 (define-key map (kbd "D") 'ampc-delete-playlist)
359 (define-key map (kbd "y") 'ampc-increase-volume)
360 (define-key map (kbd "M-y") 'ampc-decrease-volume)
361 (define-key map (kbd "h") 'ampc-increase-crossfade)
362 (define-key map (kbd "M-h") 'ampc-decrease-crossfade)
363 (define-key map (kbd "e") 'ampc-toggle-repeat)
364 (define-key map (kbd "r") 'ampc-toggle-random)
365 (define-key map (kbd "f") 'ampc-toggle-consume)
366 (define-key map (kbd "P") 'ampc-goto-current-song)
367 (define-key map (kbd "q") 'ampc-quit)
368 (define-key map (kbd "z") 'ampc-suspend)
369 (define-key map (kbd "T") 'ampc-trigger-update)
370 (loop for view in ampc-views
371 do (define-key map (car view)
374 (ampc-configure-frame ',(cdr view)))))
377 (defvar ampc-item-mode-map
378 (let ((map (make-sparse-keymap)))
379 (suppress-keymap map)
380 (define-key map (kbd "m") 'ampc-mark)
381 (define-key map (kbd "u") 'ampc-unmark)
382 (define-key map (kbd "U") 'ampc-unmark-all)
383 (define-key map (kbd "n") 'ampc-next-line)
384 (define-key map (kbd "p") 'ampc-previous-line)
387 (defvar ampc-current-playlist-mode-map
388 (let ((map (make-sparse-keymap)))
389 (suppress-keymap map)
390 (define-key map (kbd "<return>") 'ampc-play-this)
393 (defvar ampc-playlist-mode-map
394 (let ((map (make-sparse-keymap)))
395 (suppress-keymap map)
396 (define-key map (kbd "t") 'ampc-toggle-marks)
397 (define-key map (kbd "d") 'ampc-delete)
398 (define-key map (kbd "<up>") 'ampc-up)
399 (define-key map (kbd "<down>") 'ampc-down)
402 (defvar ampc-playlists-mode-map
403 (let ((map (make-sparse-keymap)))
404 (suppress-keymap map)
405 (define-key map (kbd "l") 'ampc-load)
406 (define-key map (kbd "r") 'ampc-rename-playlist)
407 (define-key map (kbd "d") 'ampc-delete-playlist)
410 (defvar ampc-tag-song-mode-map
411 (let ((map (make-sparse-keymap)))
412 (suppress-keymap map)
413 (define-key map (kbd "t") 'ampc-toggle-marks)
414 (define-key map (kbd "a") 'ampc-add)
417 (defvar ampc-outputs-mode-map
418 (let ((map (make-sparse-keymap)))
419 (suppress-keymap map)
420 (define-key map (kbd "t") 'ampc-toggle-marks)
421 (define-key map (kbd "a") 'ampc-toggle-output-enabled)
425 (easy-menu-define ampc-menu ampc-mode-map
428 ["Play" ampc-toggle-play
429 :visible (and ampc-status
430 (not (equal (cdr (assq 'state ampc-status)) "play")))]
431 ["Pause" ampc-toggle-play
432 :visible (and ampc-status
433 (equal (cdr (assq 'state ampc-status)) "play"))]
435 ["Clear playlist" ampc-clear]
436 ["Shuffle playlist" ampc-shuffle]
437 ["Store playlist" ampc-store]
438 ["Queue Playlist" ampc-load :visible (ampc-playlist)]
439 ["Rename Playlist" ampc-rename-playlist :visible (ampc-playlist)]
440 ["Delete Playlist" ampc-delete-playlist :visible (ampc-playlist)]
442 ["Increase volume" ampc-increase-volume]
443 ["Decrease volume" ampc-decrease-volume]
444 ["Increase crossfade" ampc-increase-crossfade]
445 ["Decrease crossfade" ampc-decrease-crossfade]
446 ["Toggle repeat" ampc-toggle-repeat]
447 ["Toggle random" ampc-toggle-random]
448 ["Toggle consume" ampc-toggle-consume]
450 ["Trigger update" ampc-trigger-update]
453 (easy-menu-define ampc-selection-menu ampc-item-mode-map
454 "Selection menu for ampc"
456 ["Add to playlist" ampc-add
457 :visible (not (eq (car ampc-type) 'outputs))]
458 ["Toggle enabled" ampc-toggle-output-enabled
459 :visible (eq (car ampc-type) 'outputs)]
461 ["Next line" ampc-next-line]
462 ["Previous line" ampc-previous-line]
464 ["Unmark" ampc-unmark]
465 ["Unmark all" ampc-unmark-all]
466 ["Toggle marks" ampc-toggle-marks
467 :visible (not (eq (car ampc-type) 'playlists))]))
471 (defmacro ampc-with-buffer (type &rest body)
472 (declare (indent 1) (debug t))
473 `(let* ((type- ,type)
474 (b (loop for b in ampc-buffers
475 when (with-current-buffer b
476 (cond ((windowp type-)
477 (eq (window-buffer type-)
480 (eq (car ampc-type) type-))
482 (equal ampc-type type-))))
486 (with-current-buffer b
487 (let ((buffer-read-only))
488 ,@(if (eq (car body) 'no-se)
491 (goto-char (point-min))
494 (defmacro ampc-fill-skeleton (tag &rest body)
495 (declare (indent 1) (debug t))
497 (data-buffer (current-buffer)))
498 (ampc-with-buffer tag-
500 (let ((point (point)))
501 (goto-char (point-min))
503 do (put-text-property (point) (1+ (point)) 'updated t)
505 (goto-char (point-min))
507 (goto-char (point-min))
509 when (get-text-property (point) 'updated)
510 do (delete-region (point) (1+ (line-end-position)))
512 do (forward-line nil)
517 (with-selected-window (if (windowp tag-) tag- (ampc-get-window tag-))
520 (defmacro ampc-with-selection (arg &rest body)
521 (declare (indent 1) (debug t))
525 (goto-char (point-min))
526 (search-forward-regexp "^* " nil t)))
527 (loop initially (goto-char (point-min))
528 finally (ampc-align-point)
529 while (search-forward-regexp "^* " nil t)
534 for index from 0 to (1- (prefix-numeric-value arg-))
536 (goto-char (line-end-position))
538 until (ampc-next-line)))))
541 (define-derived-mode ampc-outputs-mode ampc-item-mode "ampc-o"
544 (define-derived-mode ampc-tag-song-mode ampc-item-mode "ampc-ts"
547 (define-derived-mode ampc-current-playlist-mode ampc-playlist-mode "ampc-cpl"
550 (define-derived-mode ampc-playlist-mode ampc-item-mode "ampc-pl"
553 (define-derived-mode ampc-playlists-mode ampc-item-mode "ampc-pls"
556 (define-derived-mode ampc-item-mode ampc-mode ""
559 (define-derived-mode ampc-mode fundamental-mode "ampc"
561 (buffer-disable-undo)
562 (setf buffer-read-only t
563 truncate-lines ampc-truncate-lines
564 font-lock-defaults '((("^\\(\\*\\)\\(.*\\)$"
566 (2 'ampc-marked-face))
567 ("^ .*$" 0 'ampc-face))
570 (define-minor-mode ampc-highlight-current-song-mode ""
574 (funcall (if ampc-highlight-current-song-mode
575 'font-lock-add-keywords
576 'font-lock-remove-keywords)
578 '((ampc-find-current-song
579 (1 'ampc-current-song-mark-face)
580 (2 'ampc-current-song-marked-face)))))
582 ;;; *** internal functions
583 (defun ampc-quote (string)
584 (concat "\"" (replace-regexp-in-string "\"" "\\\"" string) "\""))
588 (member (process-status ampc-connection) '(open run))))
590 (defun ampc-in-ampc-p ()
594 (defun ampc-add-impl (&optional data)
596 (loop for d in (get-text-property (line-end-position) 'data)
597 do (ampc-add-impl d)))
599 (avl-tree-mapc (lambda (e) (ampc-add-impl (cdr e))) data))
602 (ampc-send-command 'playlistadd
604 (ampc-quote (ampc-playlist))
606 (ampc-send-command 'add t (ampc-quote data))))
609 do (ampc-add-impl (cdr (assoc "file" d)))))))
611 (defun* ampc-skip (N &aux (song (cdr-safe (assq 'song ampc-status))))
613 (ampc-send-command 'play nil (max 0 (+ (string-to-number song) N)))))
615 (defun* ampc-find-current-song
616 (limit &aux (point (point)) (song (cdr-safe (assq 'song ampc-status))))
618 (<= (1- (line-number-at-pos (point)))
619 (setf song (string-to-number song)))
620 (>= (1- (line-number-at-pos limit)) song))
621 (goto-char (point-min))
624 (narrow-to-region (max point (point)) (min limit (line-end-position)))
625 (search-forward-regexp "\\(?1:\\(\\`\\*\\)?\\)\\(?2:.*\\)$"))))
627 (defun ampc-set-volume (arg func)
628 (when (or arg ampc-status)
632 (or (and arg (prefix-numeric-value arg))
633 (max (min (funcall func
635 (cdr (assq 'volume ampc-status)))
640 (defun ampc-set-crossfade (arg func)
641 (when (or arg ampc-status)
645 (or (and arg (prefix-numeric-value arg))
647 (string-to-number (cdr (assq 'xfade ampc-status)))
651 (defun* ampc-fix-pos (f &aux buffer-read-only)
653 (move-beginning-of-line nil)
654 (let* ((data (get-text-property (+ 2 (point)) 'data))
655 (pos (assoc "Pos" data)))
656 (setf (cdr pos) (funcall f (cdr pos)))
657 (put-text-property (+ 2 (point))
662 (defun* ampc-move-impl (up &aux (line (1- (line-number-at-pos))))
663 (when (or (and up (eq line 0))
664 (and (not up) (eq (1+ line) (line-number-at-pos (1- (point-max))))))
665 (return-from ampc-move-impl t))
667 (move-beginning-of-line nil)
669 (ampc-send-command 'playlistmove
671 (ampc-quote (ampc-playlist))
673 (funcall (if up '1- '1+)
675 (ampc-send-command 'move nil line (funcall (if up '1- '1+) line)))
678 (unless (ampc-playlist)
683 (let ((buffer-read-only))
684 (transpose-lines 1)))
690 (defun* ampc-move (up N &aux (point (point)))
691 (goto-char (if up (point-min) (point-max)))
693 (funcall (if up 'search-forward-regexp 'search-backward-regexp)
697 (loop until (ampc-move-impl up)
699 do (search-backward-regexp "^* " nil t)
701 until (not (funcall (if up
702 'search-forward-regexp
703 'search-backward-regexp)
714 (unless (eq (1- N) 0)
715 (setf N (- (- (forward-line (1- N)) (1- N))))))
717 until (ampc-move-impl up)))))
719 (defun ampc-toggle-state (state arg)
720 (when (or arg ampc-status)
725 (if (equal (cdr (assq state ampc-status)) "1")
728 ((> (prefix-numeric-value arg) 0) 1)
731 (defun ampc-playlist ()
732 (ampc-with-buffer 'playlists
733 (if (search-forward-regexp "^* \\(.*\\)$" nil t)
736 (buffer-substring-no-properties
737 (+ (line-beginning-position) 2)
738 (line-end-position))))))
740 (defun* ampc-mark-impl (select N &aux result buffer-read-only)
741 (when (eq (car ampc-type) 'playlists)
742 (assert (or (not select) (null N) (eq N 1)))
743 (ampc-with-buffer 'playlists
744 (loop while (search-forward-regexp "^\\* " nil t)
745 do (replace-match " " nil nil))))
746 (loop repeat (or N 1)
748 do (move-beginning-of-line nil)
750 (insert (if select "*" " "))
751 (setf result (ampc-next-line nil)))
752 (ampc-post-mark-change-update)
755 (defun ampc-post-mark-change-update ()
756 (ecase (car ampc-type)
757 ((current-playlist playlist outputs))
759 (ampc-update-playlist))
761 (loop for w in (ampc-windows)
764 do (with-current-buffer (window-buffer w)
765 (when (member (car ampc-type) '(song tag))
768 if (eq w (selected-window))
771 (ampc-fill-tag-song))))
773 (defun ampc-align-point ()
775 (move-beginning-of-line nil)
778 (defun ampc-pad (alist)
779 (loop for (offset . data) in alist
781 with current-offset = 0
782 when (<= current-offset offset)
783 when (and (not first) (eq (- offset current-offset) 0))
786 and concat (make-string (- offset current-offset) ? )
787 and do (setf current-offset offset)
790 and do (incf current-offset)
793 do (setf current-offset (+ current-offset (length data))
796 (defun ampc-update-header ()
797 (if (eq (car ampc-type) 'status)
798 (setf header-line-format nil)
799 (setf header-line-format
801 (make-string (floor (fringe-columns 'left t)) ? )
802 (ecase (car ampc-type)
804 (concat " " (plist-get (cdr ampc-type) :tag)))
808 (ampc-pad (loop for p in (plist-get (cdr ampc-type) :properties)
809 collect `(,(or (plist-get (cdr p) :offset) 2) .
810 ,(or (plist-get (cdr p) :title)
813 " [ Updating... ]")))))
815 (defun ampc-set-dirty (tag-or-dirty &optional dirty)
816 (if (or (null tag-or-dirty) (eq tag-or-dirty t))
817 (progn (setf ampc-dirty tag-or-dirty)
818 (ampc-update-header))
819 (loop for w in (ampc-windows)
820 do (with-current-buffer (window-buffer w)
821 (when (eq (car ampc-type) tag-or-dirty)
822 (ampc-set-dirty dirty))))))
824 (defun ampc-update ()
826 (loop for b in ampc-buffers
827 do (with-current-buffer b
829 (ecase (car ampc-type)
831 (ampc-send-command 'outputs))
833 (ampc-update-playlist))
835 (if (assoc (ampc-tags) ampc-internal-db)
837 (push `(,(ampc-tags) . ,(ampc-create-tree))
839 (ampc-send-command 'listallinfo)))
841 (ampc-send-command 'status)
842 (ampc-send-command 'currentsong))
844 (ampc-send-command 'listplaylists))
846 (ampc-send-command 'playlistinfo))))))
847 (ampc-send-command 'status)
848 (ampc-send-command 'currentsong)))
850 (defun ampc-update-playlist ()
851 (ampc-with-buffer 'playlists
852 (if (search-forward-regexp "^\\* " nil t)
853 (ampc-send-command 'listplaylistinfo
855 (get-text-property (point) 'data))
856 (ampc-with-buffer 'playlist
857 (delete-region (point-min) (point-max))
858 (ampc-set-dirty nil)))))
860 (defun ampc-send-command-impl (command)
862 (message (concat "ampc: " command)))
863 (process-send-string ampc-connection (concat command "\n")))
865 (defun ampc-send-command (command &optional unique &rest args)
866 (if (equal command 'idle)
867 (when ampc-working-timer
868 (cancel-timer ampc-working-timer)
870 ampc-working-timer nil)
872 (unless ampc-working-timer
874 ampc-working-timer (run-at-time nil 0.1 'ampc-yield))))
875 (setf command `(,command ,@args))
876 (when (equal (car-safe ampc-outstanding-commands) '(idle))
877 (setf (car ampc-outstanding-commands) '(noidle))
878 (ampc-send-command-impl "noidle"))
879 (setf ampc-outstanding-commands
881 ampc-outstanding-commands
882 (remove command ampc-outstanding-commands))
885 (defun ampc-send-next-command ()
886 (unless ampc-outstanding-commands
887 (ampc-send-command 'idle))
888 (ampc-send-command-impl (concat (symbol-name (caar ampc-outstanding-commands))
890 (cdar ampc-outstanding-commands)
892 concat (cond ((integerp a)
893 (number-to-string a))
896 (defun ampc-tree< (a b)
897 (not (string< (if (listp a) (car a) a) (if (listp b) (car b) b))))
899 (defun ampc-create-tree ()
900 (avl-tree-create 'ampc-tree<))
902 (defun ampc-extract (tag &optional buffer)
903 (with-current-buffer (or buffer (current-buffer))
905 (ampc-extract (plist-get tag :tag))
907 (goto-char (point-min))
908 (when (search-forward-regexp
909 (concat "^" (regexp-quote tag) ": \\(.*\\)$")
912 (let ((result (match-string 1)))
913 (when (equal tag "Time")
914 (setf result (ampc-transform-time result)))
917 (defun ampc-insert (element data &optional cmp)
919 (goto-char (point-min))
922 for tp = (get-text-property (+ (point) 2) 'data)
923 finally return 'insert
926 (let ((s (buffer-substring-no-properties
928 (line-end-position))))
929 (cond ((equal s element)
930 (unless (member data tp)
931 (put-text-property (+ (point) 2)
932 (1+ (line-end-position))
939 (let ((r (funcall cmp data tp)))
940 (if (memq r '(update insert))
942 (forward-line (1- r))
947 (let ((s (buffer-substring-no-properties
949 (line-end-position))))
950 (unless (string< s element)
955 (let ((start (point)))
956 (insert element "\n")
957 (put-text-property start (point) 'data (if (eq cmp t)
961 (remove-text-properties (point) (1+ (point)) '(updated))
962 (equal (buffer-substring (point) (1+ (point))) "*")))))
964 (defun ampc-fill-tag (trees)
965 (put-text-property (point-min) (point-max) 'data nil)
967 finally return new-trees
969 do (avl-tree-mapc (lambda (e)
970 (when (ampc-insert (car e) (cdr e) t)
971 (push (cdr e) new-trees)))
974 (defun ampc-fill-song (trees)
977 do (loop for song in songs
980 (loop for (p . v) in (plist-get (cdr ampc-type) :properties)
981 collect `(,(- (or (plist-get v :offset) 2) 2)
982 . ,(or (cdr-safe (assoc p song)) ""))))
985 (defun* ampc-narrow-entry (&optional (delimiter "file"))
986 (narrow-to-region (move-beginning-of-line nil)
987 (or (progn (goto-char (line-end-position))
988 (when (search-forward-regexp
989 (concat "^" (regexp-quote delimiter) ": ")
992 (move-beginning-of-line nil)
996 (defun ampc-get-window (type)
997 (loop for w in (ampc-windows)
998 thereis (with-current-buffer (window-buffer w)
999 (when (eq (car ampc-type) type)
1002 (defun* ampc-fill-playlist (&aux properties)
1003 (ampc-fill-skeleton 'playlist
1004 (setf properties (plist-get (cdr ampc-type) :properties))
1005 (with-current-buffer data-buffer
1008 while (search-forward-regexp "^file: " nil t)
1009 do (save-restriction
1011 (let ((file (ampc-extract "file"))
1014 (loop for (tag . tag-properties) in properties
1015 collect `(,(- (or (plist-get tag-properties
1019 . ,(or (ampc-extract tag)
1020 "[Not Specified]"))))))
1021 (ampc-with-buffer 'playlist
1026 (let ((p1 (cdr (assoc 'index a)))
1027 (p2 (cdr (assoc 'index b))))
1028 (cond ((< p1 p2) 'update)
1030 (if (equal (cdr (assoc "file" a))
1031 (cdr (assoc "file" b)))
1034 (t (- p1 p2)))))))))))))
1036 (defun* ampc-fill-outputs (&aux properties)
1037 (ampc-fill-skeleton 'outputs
1038 (setf properties (plist-get (cdr ampc-type) :properties))
1039 (with-current-buffer data-buffer
1041 while (search-forward-regexp "^outputid: " nil t)
1042 do (save-restriction
1043 (ampc-narrow-entry "outputid")
1044 (let ((outputid (ampc-extract "outputid"))
1045 (outputenabled (ampc-extract "outputenabled"))
1048 (loop for (tag . tag-properties) in properties
1049 collect `(,(- (or (plist-get tag-properties :offset)
1052 . ,(ampc-extract tag))))))
1053 (ampc-with-buffer 'outputs
1054 (ampc-insert text `(("outputid" . ,outputid)
1055 ("outputenabled" . ,outputenabled))))))))))
1057 (defun* ampc-fill-current-playlist (&aux properties)
1058 (ampc-fill-skeleton 'current-playlist
1059 (setf properties (plist-get (cdr ampc-type) :properties))
1060 (with-current-buffer data-buffer
1062 while (search-forward-regexp "^file: " nil t)
1063 do (save-restriction
1065 (let ((file (ampc-extract "file"))
1066 (pos (ampc-extract "Pos"))
1069 (loop for (tag . tag-properties) in properties
1070 collect `(,(- (or (plist-get tag-properties :offset)
1073 . ,(or (ampc-extract tag)
1074 "[Not Specified]"))))))
1075 (ampc-with-buffer 'current-playlist
1078 ("Pos" . ,(string-to-number pos)))
1080 (let ((p1 (cdr (assoc "Pos" a)))
1081 (p2 (cdr (assoc "Pos" b))))
1082 (cond ((< p1 p2) 'insert)
1084 (if (equal (cdr (assoc "file" a))
1085 (cdr (assoc "file" b)))
1088 (t (- p1 p2)))))))))))))
1090 (defun ampc-fill-playlists ()
1091 (ampc-fill-skeleton 'playlists
1092 (with-current-buffer data-buffer
1093 (loop while (search-forward-regexp "^playlist: \\(.*\\)$" nil t)
1094 for playlist = (match-string 1)
1095 do (ampc-with-buffer 'playlists
1096 (ampc-insert playlist playlist))))))
1098 (defun ampc-yield ()
1099 (setf ampc-yield (1+ ampc-yield))
1102 (defun ampc-fill-status ()
1103 (ampc-with-buffer 'status
1104 (delete-region (point-min) (point-max))
1105 (funcall (or (plist-get (cadr ampc-type) :filler)
1107 (insert (ampc-status) "\n")))
1109 (ampc-set-dirty nil)))
1111 (defun ampc-fill-tag-song ()
1113 with trees = `(,(cdr (assoc (ampc-tags) ampc-internal-db)))
1114 for w in (ampc-windows)
1117 (when (member (car ampc-type) '(tag song))
1119 (ampc-fill-skeleton w
1120 (ecase (car ampc-type)
1121 (tag (setf trees (ampc-fill-tag trees)))
1122 (song (ampc-fill-song trees))))
1124 (loop while (search-forward-regexp "^* " nil t)
1125 do (setf trees (append (get-text-property (point) 'data)
1128 (defun* ampc-transform-time (data &aux (time (string-to-number data)))
1129 (concat (number-to-string (/ time 60))
1131 (when (< (% time 60) 10)
1133 (number-to-string (% time 60))))
1135 (defun ampc-handle-idle ()
1137 for subsystem = (buffer-substring (point) (line-end-position))
1138 when (string-match "^changed: \\(.*\\)$" subsystem)
1139 do (case (intern (match-string 1 subsystem))
1141 (setf ampc-internal-db nil)
1142 (ampc-set-dirty 'tag t)
1143 (ampc-set-dirty 'song t))
1145 (ampc-set-dirty 'outputs t))
1146 ((player options mixer)
1147 (setf ampc-status nil)
1148 (ampc-set-dirty 'status t))
1150 (ampc-set-dirty 'playlists t)
1151 (ampc-set-dirty 'playlist t))
1153 (ampc-set-dirty 'current-playlist t)
1154 (ampc-set-dirty 'status t)))
1159 (defun ampc-handle-setup (status)
1160 (unless (and (string-match "^ MPD \\(.+\\)\\.\\(.+\\)\\.\\(.+\\)$"
1162 (let ((version-a (string-to-number (match-string 1 status)))
1163 (version-b (string-to-number (match-string 2 status)))
1164 ;; (version-c (string-to-number (match-string 2 status)))
1167 (>= version-b 15))))
1168 (error (concat "Your version of MPD is not supported. "
1169 "ampc supports MPD (protocol version) 0.15.0 "
1172 (defun ampc-fill-internal-db (running)
1173 (loop for origin = (and (search-forward-regexp "^file: " nil t)
1174 (line-beginning-position))
1179 (and (search-forward-regexp "^file: " nil t)
1180 (move-beginning-of-line nil)))
1182 do (save-restriction
1183 (narrow-to-region origin next)
1184 (ampc-fill-internal-db-entry))
1187 (delete-region origin next)
1188 (setf next origin))))
1191 (loop for w in (ampc-windows)
1192 for tag = (with-current-buffer (window-buffer w)
1193 (when (eq (car ampc-type) 'tag)
1194 (plist-get (cdr ampc-type) :tag)))
1199 (defun ampc-fill-internal-db-entry ()
1201 with data-buffer = (current-buffer)
1202 with tree = `(nil . ,(cdr (assoc (ampc-tags) ampc-internal-db)))
1203 for w in (ampc-windows)
1205 (with-current-buffer (window-buffer w)
1207 (ecase (car ampc-type)
1209 (let* ((data (or (ampc-extract (cdr ampc-type) data-buffer)
1211 (member (and (cdr tree) (avl-tree-member (cdr tree) data))))
1212 (cond (member (setf tree member))
1214 (setf member `(,data . nil))
1215 (avl-tree-enter (cdr tree) member)
1218 (setf (cdr tree) (ampc-create-tree) member`(,data . nil))
1219 (avl-tree-enter (cdr tree) member)
1220 (setf tree member)))))
1222 (push (loop for p in `(("file")
1223 ,@(plist-get (cdr ampc-type) :properties))
1224 for data = (ampc-extract (car p) data-buffer)
1226 collect `(,(car p) . ,data)
1231 (defun ampc-handle-current-song ()
1232 (loop for k in (append ampc-status-tags '("Artist" "Title"))
1233 for s = (ampc-extract k)
1235 do (push `(,(intern k) . ,s) ampc-status)
1238 (run-hook-with-args ampc-status-changed-hook ampc-status))
1240 (defun ampc-handle-status ()
1241 (loop for k in '("volume" "repeat" "random" "consume" "xfade" "state" "song")
1242 for v = (ampc-extract k)
1244 do (push `(,(intern k) . ,v) ampc-status)
1246 (ampc-with-buffer 'current-playlist
1247 (when ampc-highlight-current-song-mode
1248 (font-lock-fontify-region (point-min) (point-max)))))
1250 (defun ampc-handle-update ()
1251 (message "Database update started"))
1253 (defun ampc-handle-command (status)
1256 (pop ampc-outstanding-commands))
1257 ((eq status 'running)
1258 (case (caar ampc-outstanding-commands)
1259 (listallinfo (ampc-fill-internal-db t))))
1261 (case (car (pop ampc-outstanding-commands))
1265 (ampc-handle-setup status))
1267 (ampc-handle-current-song))
1269 (ampc-handle-status))
1271 (ampc-handle-update))
1273 (ampc-fill-playlist))
1275 (ampc-fill-playlists))
1277 (ampc-fill-current-playlist))
1279 (ampc-fill-internal-db nil))
1281 (ampc-fill-outputs)))
1282 (unless ampc-outstanding-commands
1285 (defun ampc-filter (_process string)
1286 (assert (buffer-live-p (process-buffer ampc-connection)))
1287 (with-current-buffer (process-buffer ampc-connection)
1290 (message "ampc: -> %s" string))
1291 (goto-char (process-mark ampc-connection))
1293 (set-marker (process-mark ampc-connection) (point)))
1295 (goto-char (point-min))
1297 (if (or (and (search-forward-regexp
1298 "^ACK \\[\\(.*\\)\\] {.*} \\(.*\\)\n\\'"
1301 (message "ampc command error: %s (%s)"
1305 (and (search-forward-regexp "^OK\\(.*\\)\n\\'" nil t)
1308 (let ((match-end (match-end 0)))
1310 (narrow-to-region (point-min) match-end)
1311 (goto-char (point-min))
1312 (ampc-handle-command (if success (match-string 1) 'error)))
1313 (delete-region (point-min) match-end))
1314 (ampc-send-next-command))
1315 (ampc-handle-command 'running))))))
1317 ;;; **** window management
1318 (defun ampc-windows (&optional unordered)
1319 (loop for f being the frame
1320 thereis (loop for w being the windows of f
1321 when (eq (window-buffer w) (car-safe ampc-buffers))
1322 return (loop for b in (if unordered
1323 ampc-buffers-unordered
1326 (loop for w being the windows of f
1327 thereis (and (eq (window-buffer w)
1331 (defun* ampc-configure-frame-1 (split &aux (split-type (car split)))
1332 (if (member split-type '(vertical horizontal))
1334 (loop with length = (if (eq split-type 'horizontal)
1339 for subsplit in (cdr split)
1340 for s = (car subsplit)
1343 and do (setf rest-car sizes)
1345 do (let ((l (if (integerp s) s (floor (* s length)))))
1346 (setf rest (- rest l))
1348 finally do (setf (car rest-car) rest))
1349 (let ((first-window (selected-window)))
1350 (setf sizes (nreverse sizes))
1351 (loop for size in (loop for s in sizes
1353 for window on (cdr sizes)
1358 (eq split-type 'horizontal)))))
1359 (setf (car sizes) first-window))
1360 (loop for subsplit in (cdr split)
1362 do (with-selected-window window
1363 (ampc-configure-frame-1 (cdr subsplit)))
1364 if (plist-get (cddr subsplit) :point)
1365 do (select-window window)
1367 (setf (window-dedicated-p (selected-window)) nil)
1370 (pop-to-buffer-same-window
1371 (get-buffer-create (concat "*ampc "
1372 (or (plist-get (cdr split) :tag) "Song")
1374 (ampc-tag-song-mode))
1376 (pop-to-buffer-same-window (get-buffer-create "*ampc Outputs*"))
1377 (ampc-outputs-mode))
1379 (pop-to-buffer-same-window (get-buffer-create "*ampc Current Playlist*"))
1380 (ampc-current-playlist-mode)
1381 (ampc-highlight-current-song-mode 1))
1383 (pop-to-buffer-same-window (get-buffer-create "*ampc Playlist*"))
1384 (ampc-playlist-mode))
1386 (pop-to-buffer-same-window (get-buffer-create "*ampc Playlists*"))
1387 (ampc-playlists-mode))
1389 (pop-to-buffer-same-window (get-buffer-create "*ampc Status*"))
1391 (destructuring-bind (&key (dedicated t) (mode-line t) &allow-other-keys)
1393 (setf (window-dedicated-p (selected-window)) dedicated)
1395 (setf mode-line-format nil)))
1396 (setf ampc-type split)
1397 (add-to-list 'ampc-all-buffers (current-buffer))
1398 (push `(,(or (plist-get (cdr split) :id)
1399 (if (eq (car ampc-type) 'song) 9998 9999))
1400 . ,(current-buffer))
1402 (ampc-set-dirty t)))
1404 (defun ampc-configure-frame (split)
1405 (if ampc-use-full-frame
1406 (progn (setf (window-dedicated-p (selected-window)) nil)
1407 (delete-other-windows))
1408 (loop with live-window = nil
1409 for w in (nreverse (ampc-windows t))
1410 if (window-live-p w)
1411 if (not live-window)
1412 do (setf live-window w)
1414 do (delete-window w)
1417 finally do (if live-window (select-window live-window))))
1418 (setf ampc-buffers nil)
1419 (ampc-configure-frame-1 split)
1420 (setf ampc-buffers-unordered (mapcar 'cdr ampc-buffers)
1421 ampc-buffers (mapcar 'cdr (sort ampc-buffers
1422 (lambda (a b) (< (car a) (car b))))))
1425 ;;; *** interactives
1426 (defun* ampc-unmark-all (&aux buffer-read-only)
1429 (assert (ampc-in-ampc-p))
1431 (goto-char (point-min))
1432 (loop while (search-forward-regexp "^\\* " nil t)
1433 do (replace-match " " nil nil)))
1434 (ampc-post-mark-change-update))
1436 (defun ampc-trigger-update ()
1437 "Trigger a database update."
1439 (assert (ampc-on-p))
1440 (ampc-send-command 'update))
1442 (defun* ampc-toggle-marks (&aux buffer-read-only)
1443 "Toggle marks. Marked entries become unmarked, and vice versa."
1445 (assert (ampc-in-ampc-p))
1447 (loop for (a . b) in '(("* " . "T ")
1450 do (goto-char (point-min))
1451 (loop while (search-forward-regexp (concat "^" (regexp-quote a))
1454 do (replace-match b nil nil))))
1455 (ampc-post-mark-change-update))
1457 (defun ampc-up (&optional arg)
1458 "Go to the previous ARG'th entry.
1459 With optional prefix ARG, move the next ARG entries after point
1460 rather than the selection."
1462 (assert (ampc-in-ampc-p))
1465 (defun ampc-down (&optional arg)
1466 "Go to the next ARG'th entry.
1467 With optional prefix ARG, move the next ARG entries after point
1468 rather than the selection."
1470 (assert (ampc-in-ampc-p))
1471 (ampc-move nil arg))
1473 (defun ampc-mark (&optional arg)
1474 "Mark the next ARG'th entries.
1477 (assert (ampc-in-ampc-p))
1478 (ampc-mark-impl t arg))
1480 (defun ampc-unmark (&optional arg)
1481 "Unmark the next ARG'th entries.
1484 (assert (ampc-in-ampc-p))
1485 (ampc-mark-impl nil arg))
1487 (defun ampc-increase-volume (&optional arg)
1489 With prefix argument ARG, set volume to ARG percent."
1491 (assert (ampc-on-p))
1492 (ampc-set-volume arg '+))
1494 (defun ampc-decrease-volume (&optional arg)
1496 With prefix argument ARG, set volume to ARG percent."
1498 (assert (ampc-on-p))
1499 (ampc-set-volume arg '-))
1501 (defun ampc-increase-crossfade (&optional arg)
1502 "Increase crossfade.
1503 With prefix argument ARG, set crossfading to ARG seconds."
1505 (assert (ampc-on-p))
1506 (ampc-set-crossfade arg '+))
1508 (defun ampc-decrease-crossfade (&optional arg)
1509 "Decrease crossfade.
1510 With prefix argument ARG, set crossfading to ARG seconds."
1512 (assert (ampc-on-p))
1513 (ampc-set-crossfade arg '-))
1515 (defun ampc-toggle-repeat (&optional arg)
1516 "Toggle MPD's repeat state.
1517 With prefix argument ARG, enable repeating if ARG is positive,
1518 otherwise disable it."
1520 (assert (ampc-on-p))
1521 (ampc-toggle-state 'repeat arg))
1523 (defun ampc-toggle-consume (&optional arg)
1524 "Toggle MPD's consume state.
1525 With prefix argument ARG, enable consuming if ARG is positive,
1526 otherwise disable it.
1528 When consume is activated, each song played is removed from the playlist."
1530 (assert (ampc-on-p))
1531 (ampc-toggle-state 'consume arg))
1533 (defun ampc-toggle-random (&optional arg)
1534 "Toggle MPD's random state.
1535 With prefix argument ARG, enable random playing if ARG is positive,
1536 otherwise disable it."
1538 (ampc-toggle-state 'random arg))
1540 (defun ampc-play-this ()
1541 "Play selected song."
1543 (assert (ampc-in-ampc-p))
1545 (ampc-send-command 'play nil (1- (line-number-at-pos)))
1546 (ampc-send-command 'pause nil 0)))
1548 (defun* ampc-toggle-play
1549 (&optional arg &aux (state (cdr-safe (assq 'state ampc-status))))
1551 If mpd does not play a song already, start playing the song at
1552 point if the current buffer is the playlist buffer, otherwise
1553 start at the beginning of the playlist.
1555 If ARG is 4, stop player rather than pause if applicable."
1557 (assert (ampc-on-p))
1560 (setf arg (prefix-numeric-value arg)))
1561 (ecase (intern state)
1563 (when (or (null arg) (> arg 0))
1567 (if (and (eq (car ampc-type) 'current-playlist) (not (eobp)))
1568 (1- (line-number-at-pos))
1571 (when (or (null arg) (> arg 0))
1572 (ampc-send-command 'pause nil 0)))
1574 (cond ((or (null arg) (< arg 0))
1575 (ampc-send-command 'pause nil 1))
1577 (ampc-send-command 'stop)))))))
1579 (defun ampc-next (&optional arg)
1581 With prefix argument ARG, skip ARG songs."
1583 (assert (ampc-on-p))
1584 (ampc-skip (or arg 1)))
1586 (defun ampc-previous (&optional arg)
1587 "Play previous song.
1588 With prefix argument ARG, skip ARG songs."
1590 (assert (ampc-on-p))
1591 (ampc-skip (- (or arg 1))))
1593 (defun ampc-rename-playlist (new-name)
1594 "Rename selected playlist to NEW-NAME.
1595 Interactively, read NEW-NAME from the minibuffer."
1596 (interactive "MNew name: ")
1597 (assert (ampc-in-ampc-p))
1599 (ampc-send-command 'rename nil (ampc-playlist) new-name)
1600 (error "No playlist selected")))
1603 "Load selected playlist in the current playlist."
1605 (assert (ampc-in-ampc-p))
1607 (ampc-send-command 'load nil (ampc-quote (ampc-playlist)))
1608 (error "No playlist selected")))
1610 (defun ampc-toggle-output-enabled (&optional arg)
1611 "Toggle the next ARG outputs.
1612 If ARG is omitted, use the selected entries."
1614 (assert (ampc-in-ampc-p))
1615 (ampc-with-selection arg
1616 (let ((data (get-text-property (point) 'data)))
1617 (ampc-send-command (if (equal (cdr (assoc "outputenabled" data)) "1")
1621 (cdr (assoc "outputid" data))))))
1623 (defun ampc-delete (&optional arg)
1624 "Delete the next ARG songs from the playlist.
1625 If ARG is omitted, use the selected entries."
1627 (assert (ampc-in-ampc-p))
1628 (let ((point (point)))
1629 (ampc-with-selection arg
1630 (let ((val (1- (- (line-number-at-pos) index))))
1632 (ampc-send-command 'playlistdelete
1634 (ampc-quote (ampc-playlist))
1636 (ampc-send-command 'delete t val))))
1638 (ampc-align-point)))
1640 (defun ampc-shuffle ()
1643 (assert (ampc-on-p))
1644 (if (not (ampc-playlist))
1645 (ampc-send-command 'shuffle)
1646 (ampc-with-buffer 'playlist
1650 (sort (loop until (eobp)
1651 collect `(,(cdr (assoc "file" (get-text-property
1657 (< (cdr a) (cdr b)))))))
1659 (loop for s in shuffled
1660 do (ampc-add-impl s))))))
1662 (defun ampc-clear ()
1665 (assert (ampc-on-p))
1667 (ampc-send-command 'playlistclear nil (ampc-quote (ampc-playlist)))
1668 (ampc-send-command 'clear)))
1670 (defun ampc-add (&optional arg)
1671 "Add the next ARG songs associated with the entries after point
1673 If ARG is omitted, use the selected entries in the current buffer."
1675 (assert (ampc-in-ampc-p))
1676 (ampc-with-selection arg
1679 (defun ampc-status ()
1680 "Display the information that is displayed in the status window."
1682 (assert (ampc-on-p))
1683 (let* ((flags (mapconcat
1685 (loop for (f . n) in '((repeat . "Repeat")
1687 (consume . "Consume"))
1688 when (equal (cdr (assq f ampc-status)) "1")
1692 (state (cdr (assq 'state ampc-status)))
1693 (status (concat "State: " state
1695 (concat (make-string (- 10 (length state)) ? )
1696 (nth (% ampc-yield 4) '("|" "/" "-" "\\"))))
1698 (when (equal state "play")
1700 (or (cdr-safe (assq 'Artist ampc-status))
1703 (or (cdr-safe (assq 'Title ampc-status))
1706 "Volume: " (cdr (assq 'volume ampc-status)) "\n"
1707 "Crossfade: " (cdr (assq 'xfade ampc-status))
1708 (unless (equal flags "")
1709 (concat "\n" flags)))))
1710 (when (called-interactively-p 'interactive)
1711 (message "%s" status))
1714 (defun ampc-delete-playlist ()
1715 "Delete selected playlist."
1717 (assert (ampc-in-ampc-p))
1718 (ampc-with-selection nil
1719 (let ((name (get-text-property (point) 'data)))
1720 (when (y-or-n-p (concat "Delete playlist " name "?"))
1721 (ampc-send-command 'rm nil (ampc-quote name))))))
1723 (defun ampc-store (name)
1724 "Store current playlist as NAME.
1725 Interactively, read NAME from the minibuffer."
1726 (interactive "MSave playlist as: ")
1727 (assert (ampc-in-ampc-p))
1728 (ampc-send-command 'save nil (ampc-quote name)))
1730 (defun* ampc-goto-current-song
1731 (&aux (song (cdr-safe (assq 'song ampc-status))))
1732 "Select the current playlist window and move point to the current song."
1734 (assert (ampc-in-ampc-p))
1736 (ampc-with-buffer 'current-playlist
1738 (select-window (ampc-get-window 'current-playlist))
1739 (goto-char (point-min))
1740 (forward-line (string-to-number song))
1741 (ampc-align-point))))
1743 (defun ampc-previous-line (&optional arg)
1744 "Go to previous ARG'th entry in the current buffer.
1747 (assert (ampc-in-ampc-p))
1748 (ampc-next-line (* (or arg 1) -1)))
1750 (defun ampc-next-line (&optional arg)
1751 "Go to next ARG'th entry in the current buffer.
1754 (assert (ampc-in-ampc-p))
1757 (progn (forward-line -1)
1763 (defun* ampc-suspend (&optional (run-hook t))
1765 This function resets the window configuration, but does not close
1766 the connection to mpd or destroy the internal cache of ampc.
1767 This means subsequent startups of ampc will be faster."
1769 (when ampc-working-timer
1770 (cancel-timer ampc-working-timer))
1771 (loop with found-window
1772 for w in (nreverse (ampc-windows t))
1773 when (window-live-p w)
1775 do (delete-window w)
1777 do (setf found-window t
1778 (window-dedicated-p w) nil)
1781 (loop for b in ampc-all-buffers
1782 when (buffer-live-p b)
1785 (setf ampc-buffers nil
1786 ampc-all-buffers nil
1787 ampc-working-timer nil)
1789 (run-hooks 'ampc-suspend-hook)))
1791 (defun ampc-quit (&optional arg)
1793 If called with a prefix argument ARG, kill the mpd instance that
1794 ampc is connected to."
1797 (set-process-filter ampc-connection nil)
1798 (when (equal (car-safe ampc-outstanding-commands) '(idle))
1799 (ampc-send-command-impl "noidle")
1800 (with-current-buffer (process-buffer ampc-connection)
1801 (loop do (goto-char (point-min))
1802 until (search-forward-regexp "^\\(ACK\\)\\|\\(OK\\).*\n\\'" nil t)
1803 do (accept-process-output ampc-connection nil 50))))
1804 (ampc-send-command-impl (if arg "kill" "close")))
1805 (when ampc-working-timer
1806 (cancel-timer ampc-working-timer))
1808 (setf ampc-connection nil
1809 ampc-internal-db nil
1810 ampc-outstanding-commands nil
1812 (run-hooks 'ampc-quit-hook))
1815 (defun ampc (&optional host port)
1816 "ampc is an asynchronous client for the MPD media player.
1817 This function is the main entry point for ampc.
1819 Non-interactively, HOST and PORT specify the MPD instance to
1820 connect to. The values default to localhost:6600."
1821 (interactive "MHost (localhost): \nMPort (6600): ")
1822 (run-hooks 'ampc-before-startup-hook)
1823 (when (or (not host) (equal host ""))
1824 (setf host "localhost"))
1825 (when (or (not port) (equal port ""))
1827 (when (and ampc-connection
1828 (or (not (equal host ampc-host))
1829 (not (equal port ampc-port))
1832 (unless ampc-connection
1833 (let ((connection (open-network-stream "ampc"
1834 (with-current-buffer
1835 (get-buffer-create " *mpc*")
1836 (delete-region (point-min)
1841 :type 'plain :return-list t)))
1842 (unless (car connection)
1843 (error "Failed connecting to server: %s"
1844 (plist-get ampc-connection :error)))
1845 (setf ampc-connection (car connection)
1848 (set-process-coding-system ampc-connection 'utf-8-unix 'utf-8-unix)
1849 (set-process-filter ampc-connection 'ampc-filter)
1850 (set-process-query-on-exit-flag ampc-connection nil)
1851 (setf ampc-outstanding-commands '((setup))))
1852 (ampc-configure-frame (cdar ampc-views))
1853 (run-hooks 'ampc-connected-hook)
1854 (ampc-filter (process-buffer ampc-connection) nil))
1859 ;; eval: (outline-minor-mode 1)
1860 ;; outline-regexp: ";;; \\*+"
1861 ;; lexical-binding: t
1863 ;; indent-tabs-mode: nil