]> code.delx.au - gnu-emacs-elpa/blob - packages/ampc/ampc.el
* ampc.el (ampc): Fix typo.
[gnu-emacs-elpa] / packages / ampc / ampc.el
1 ;;; ampc.el --- Asynchronous Music Player Controller
2
3 ;; Copyright (C) 2011-2012 Free Software Foundation, Inc.
4
5 ;; Author: Christopher Schmidt <christopher@ch.ristopher.com>
6 ;; Maintainer: Christopher Schmidt <christopher@ch.ristopher.com>
7 ;; Version: 0.1.2
8 ;; Created: 2011-12-06
9 ;; Keywords: ampc, mpc, mpd
10 ;; Compatibility: GNU Emacs: 24.x
11
12 ;; This file is part of GNU Emacs.
13
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.
18
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.
23
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/>.
26
27 ;;; Commentary:
28 ;;; * description
29 ;; ampc is a controller for the Music Player Daemon (http://mpd.wikia.com/).
30
31 ;;; ** installation
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.:
35 ;;
36 ;; (add-to-list 'load-path "~/.emacs.d/ampc")
37 ;;
38 ;; Then add one autoload definition:
39 ;;
40 ;; (autoload 'ampc "ampc" nil t)
41 ;;
42 ;; Optionally bind a key to this function, e.g.:
43 ;;
44 ;; (global-set-key (kbd "<f9>") 'ampc)
45
46 ;; Byte-compile ampc (M-x byte-compile-file RET /path/to/ampc.el RET) to improve
47 ;; its performance!
48
49 ;;; ** usage
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'.
54 ;;
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'
59 ;; key.
60
61 ;;; *** current playlist view
62 ;; The playlist view should look like this
63 ;;
64 ;; .........................
65 ;; . 1 . 3 . 4 . 5 .
66 ;; .......... . . .
67 ;; . 2 . . . .
68 ;; . . . . .
69 ;; . . . . .
70 ;; . ................
71 ;; . . 6 .
72 ;; . . .
73 ;; .........................
74 ;;
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.
77 ;;
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.
80 ;;
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.
86 ;;
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
91 ;; songs down.
92 ;;
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.
97 ;;
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.
102 ;;
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.
110
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
119 ;; entry.
120 ;;
121 ;; Again, the key `<' may be used to setup a playlist view with a different
122 ;; order of tag browsers.
123
124 ;;; *** outputs view
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).
128
129 ;;; *** global keys
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:
133 ;;
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.
138 ;;
139 ;; `l' (ampc-next): Play next song.
140 ;; `j' (ampc-previous): Play previous song
141 ;;
142 ;; `c' (ampc-clear): Clear playlist.
143 ;; `s' (ampc-shuffle): Shuffle playlist.
144 ;;
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.
149 ;;
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.
154 ;;
155 ;; `e' (ampc-toggle-repeat): Toggle repeat state.
156 ;; `r' (ampc-toggle-random): Toggle random state.
157 ;; `f' (ampc-toggle-consume): Toggle consume state.
158 ;;
159 ;; `P' (ampc-goto-current-song): Select the current playlist window and move
160 ;; point to the current song.
161 ;;
162 ;; `T' (ampc-trigger-update): Trigger a database update.
163 ;; `Z' (ampc-suspend): Suspend ampc.
164 ;; `q' (ampc-quit): Quit ampc.
165 ;;
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:
170 ;;
171 ;; (eval-after-load 'ampc
172 ;; '(flet ((substitute-ampc-key
173 ;; (from to)
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 ";"))))
180 ;;
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'.
186
187 ;;; Code:
188 ;;; * code
189 (eval-when-compile
190 (require 'easymenu)
191 (require 'cl))
192 (require 'network-stream)
193 (require 'avl-tree)
194
195 ;;; ** declarations
196 ;;; *** variables
197 (defgroup ampc ()
198 "Asynchronous client for the Music Player Daemon."
199 :prefix "ampc-"
200 :group 'multimedia
201 :group 'applications)
202
203 ;;; *** customs
204 (defcustom ampc-debug nil
205 "Non-nil means log communication between ampc and MPD."
206 :type 'boolean)
207
208 (defcustom ampc-use-full-frame nil
209 "If non-nil, ampc will use the entire Emacs screen."
210 :type 'boolean)
211
212 (defcustom ampc-truncate-lines t
213 "If non-nil, truncate lines in ampc buffers."
214 :type 'boolean)
215
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'
221 command.")
222
223 ;;; **** hooks
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."
227 :type 'hook)
228
229 (defcustom ampc-connected-hook nil
230 "A hook run after ampc connected to MPD."
231 :type 'hook)
232
233 (defcustom ampc-suspend-hook nil
234 "A hook run when suspending ampc."
235 :type 'hook)
236
237 (defcustom ampc-quit-hook nil
238 "A hook run when exiting ampc."
239 :type 'hook)
240
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:
246
247 volume
248 repeat
249 random
250 consume
251 xfade
252 state
253 song
254 Artist
255 Title
256
257 and the keys in `ampc-status-tags'. Not all keys may be present
258 all the time!"
259 :type 'hook)
260
261 ;;; *** faces
262 (defface ampc-mark-face '((t (:inherit font-lock-constant-face)))
263 "Face of the mark.")
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.")
272
273 ;;; *** internal variables
274 (defvar ampc-views
275 (let* ((songs '(1.0 song :properties (("Track" :title "#")
276 ("Title" :offset 6)
277 ("Time" :offset 26))))
278 (rs_a `(1.0 vertical
279 (0.7 horizontal
280 (0.33 tag :tag "Genre" :id 1)
281 (0.33 tag :tag "Artist" :id 2)
282 (1.0 tag :tag "Album" :id 3))
283 ,songs))
284 (rs_b `(1.0 vertical
285 (0.7 horizontal
286 (0.33 tag :tag "Genre" :id 1)
287 (0.33 tag :tag "Album" :id 2)
288 (1.0 tag :tag "Artist" :id 3))
289 ,songs))
290 (pl-prop '(("Title")
291 ("Artist" :offset 20)
292 ("Album" :offset 40)
293 ("Time" :offset 60))))
294 `((,(kbd "J")
295 horizontal
296 (0.4 vertical
297 (6 status)
298 (1.0 current-playlist :properties ,pl-prop))
299 ,rs_a)
300 (,(kbd "M")
301 horizontal
302 (0.4 vertical
303 (6 status)
304 (1.0 current-playlist :properties ,pl-prop))
305 ,rs_b)
306 (,(kbd "K")
307 horizontal
308 (0.4 vertical
309 (6 status)
310 (1.0 vertical
311 (0.8 playlist :properties ,pl-prop)
312 (1.0 playlists)))
313 ,rs_a)
314 (,(kbd "<")
315 horizontal
316 (0.4 vertical
317 (6 status)
318 (1.0 vertical
319 (0.8 playlist :properties ,pl-prop)
320 (1.0 playlists)))
321 ,rs_b)
322 (,(kbd "L")
323 outputs :properties (("outputname" :title "Name")
324 ("outputenabled" :title "Enabled" :offset 10))))))
325
326 (defvar ampc-connection nil)
327 (defvar ampc-host nil)
328 (defvar ampc-port nil)
329 (defvar ampc-outstanding-commands nil)
330
331 (defvar ampc-working-timer nil)
332 (defvar ampc-yield nil)
333
334 (defvar ampc-buffers nil)
335 (defvar ampc-buffers-unordered nil)
336 (defvar ampc-all-buffers nil)
337
338 (defvar ampc-type nil)
339 (make-variable-buffer-local 'ampc-type)
340 (defvar ampc-dirty nil)
341 (make-variable-buffer-local 'ampc-dirty)
342
343 (defvar ampc-internal-db nil)
344 (defvar ampc-status nil)
345
346 ;;; *** mode maps
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)
372 `(lambda ()
373 (interactive)
374 (ampc-configure-frame ',(cdr view)))))
375 map))
376
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)
385 map))
386
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)
391 map))
392
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)
400 map))
401
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)
408 map))
409
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)
415 map))
416
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)
422 map))
423
424 ;;; **** menu
425 (easy-menu-define ampc-menu ampc-mode-map
426 "Main Menu for ampc"
427 '("ampc"
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"))]
434 "--"
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)]
441 "--"
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]
449 "--"
450 ["Trigger update" ampc-trigger-update]
451 ["Quit" ampc-quit]))
452
453 (easy-menu-define ampc-selection-menu ampc-item-mode-map
454 "Selection menu for ampc"
455 '("ampc Mark"
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)]
460 "--"
461 ["Next line" ampc-next-line]
462 ["Previous line" ampc-previous-line]
463 ["Mark" ampc-mark]
464 ["Unmark" ampc-unmark]
465 ["Unmark all" ampc-unmark-all]
466 ["Toggle marks" ampc-toggle-marks
467 :visible (not (eq (car ampc-type) 'playlists))]))
468
469 ;;; ** code
470 ;;; *** macros
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-)
478 (current-buffer)))
479 ((symbolp type-)
480 (eq (car ampc-type) type-))
481 (t
482 (equal ampc-type type-))))
483 return b
484 end)))
485 (when b
486 (with-current-buffer b
487 (let ((buffer-read-only))
488 ,@(if (eq (car body) 'no-se)
489 (cdr body)
490 `((save-excursion
491 (goto-char (point-min))
492 ,@body))))))))
493
494 (defmacro ampc-fill-skeleton (tag &rest body)
495 (declare (indent 1) (debug t))
496 `(let ((tag- ,tag)
497 (data-buffer (current-buffer)))
498 (ampc-with-buffer tag-
499 no-se
500 (let ((point (point)))
501 (goto-char (point-min))
502 (loop until (eobp)
503 do (put-text-property (point) (1+ (point)) 'updated t)
504 (forward-line))
505 (goto-char (point-min))
506 ,@body
507 (goto-char (point-min))
508 (loop until (eobp)
509 when (get-text-property (point) 'updated)
510 do (delete-region (point) (1+ (line-end-position)))
511 else
512 do (forward-line nil)
513 end)
514 (goto-char point)
515 (ampc-align-point))
516 (ampc-set-dirty nil)
517 (with-selected-window (if (windowp tag-) tag- (ampc-get-window tag-))
518 (recenter)))))
519
520 (defmacro ampc-with-selection (arg &rest body)
521 (declare (indent 1) (debug t))
522 `(let ((arg- ,arg))
523 (if (and (not arg-)
524 (save-excursion
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)
530 for index from 0
531 do (save-excursion
532 ,@body))
533 (loop until (eobp)
534 for index from 0 to (1- (prefix-numeric-value arg-))
535 do (save-excursion
536 (goto-char (line-end-position))
537 ,@body)
538 until (ampc-next-line)))))
539
540 ;;; *** modes
541 (define-derived-mode ampc-outputs-mode ampc-item-mode "ampc-o"
542 nil)
543
544 (define-derived-mode ampc-tag-song-mode ampc-item-mode "ampc-ts"
545 nil)
546
547 (define-derived-mode ampc-current-playlist-mode ampc-playlist-mode "ampc-cpl"
548 nil)
549
550 (define-derived-mode ampc-playlist-mode ampc-item-mode "ampc-pl"
551 nil)
552
553 (define-derived-mode ampc-playlists-mode ampc-item-mode "ampc-pls"
554 nil)
555
556 (define-derived-mode ampc-item-mode ampc-mode ""
557 nil)
558
559 (define-derived-mode ampc-mode fundamental-mode "ampc"
560 nil
561 (buffer-disable-undo)
562 (setf buffer-read-only t
563 truncate-lines ampc-truncate-lines
564 font-lock-defaults '((("^\\(\\*\\)\\(.*\\)$"
565 (1 'ampc-mark-face)
566 (2 'ampc-marked-face))
567 ("^ .*$" 0 'ampc-face))
568 t)))
569
570 (define-minor-mode ampc-highlight-current-song-mode ""
571 nil
572 nil
573 nil
574 (funcall (if ampc-highlight-current-song-mode
575 'font-lock-add-keywords
576 'font-lock-remove-keywords)
577 nil
578 '((ampc-find-current-song
579 (1 'ampc-current-song-mark-face)
580 (2 'ampc-current-song-marked-face)))))
581
582 ;;; *** internal functions
583 (defun ampc-quote (string)
584 (concat "\"" (replace-regexp-in-string "\"" "\\\"" string) "\""))
585
586 (defun ampc-on-p ()
587 (and ampc-connection
588 (member (process-status ampc-connection) '(open run))))
589
590 (defun ampc-in-ampc-p ()
591 (when (ampc-on-p)
592 ampc-type))
593
594 (defun ampc-add-impl (&optional data)
595 (cond ((null data)
596 (loop for d in (get-text-property (line-end-position) 'data)
597 do (ampc-add-impl d)))
598 ((avl-tree-p data)
599 (avl-tree-mapc (lambda (e) (ampc-add-impl (cdr e))) data))
600 ((stringp data)
601 (if (ampc-playlist)
602 (ampc-send-command 'playlistadd
603 t
604 (ampc-quote (ampc-playlist))
605 data)
606 (ampc-send-command 'add t (ampc-quote data))))
607 (t
608 (loop for d in data
609 do (ampc-add-impl (cdr (assoc "file" d)))))))
610
611 (defun* ampc-skip (N &aux (song (cdr-safe (assq 'song ampc-status))))
612 (when song
613 (ampc-send-command 'play nil (max 0 (+ (string-to-number song) N)))))
614
615 (defun* ampc-find-current-song
616 (limit &aux (point (point)) (song (cdr-safe (assq 'song ampc-status))))
617 (when (and song
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))
622 (forward-line song)
623 (save-restriction
624 (narrow-to-region (max point (point)) (min limit (line-end-position)))
625 (search-forward-regexp "\\(?1:\\(\\`\\*\\)?\\)\\(?2:.*\\)$"))))
626
627 (defun ampc-set-volume (arg func)
628 (when (or arg ampc-status)
629 (ampc-send-command
630 'setvol
631 nil
632 (or (and arg (prefix-numeric-value arg))
633 (max (min (funcall func
634 (string-to-number
635 (cdr (assq 'volume ampc-status)))
636 5)
637 100)
638 0)))))
639
640 (defun ampc-set-crossfade (arg func)
641 (when (or arg ampc-status)
642 (ampc-send-command
643 'crossfade
644 nil
645 (or (and arg (prefix-numeric-value arg))
646 (max (funcall func
647 (string-to-number (cdr (assq 'xfade ampc-status)))
648 5)
649 0)))))
650
651 (defun* ampc-fix-pos (f &aux buffer-read-only)
652 (save-excursion
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))
658 (line-end-position)
659 'data
660 data))))
661
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))
666 (save-excursion
667 (move-beginning-of-line nil)
668 (if (ampc-playlist)
669 (ampc-send-command 'playlistmove
670 nil
671 (ampc-quote (ampc-playlist))
672 line
673 (funcall (if up '1- '1+)
674 line))
675 (ampc-send-command 'move nil line (funcall (if up '1- '1+) line)))
676 (unless up
677 (forward-line))
678 (unless (ampc-playlist)
679 (save-excursion
680 (forward-line -1)
681 (ampc-fix-pos '1+))
682 (ampc-fix-pos '1-))
683 (let ((buffer-read-only))
684 (transpose-lines 1)))
685 (if up
686 (ampc-align-point)
687 (ampc-next-line))
688 nil)
689
690 (defun* ampc-move (up N &aux (point (point)))
691 (goto-char (if up (point-min) (point-max)))
692 (if (and (not N)
693 (funcall (if up 'search-forward-regexp 'search-backward-regexp)
694 "^* "
695 nil
696 t))
697 (loop until (ampc-move-impl up)
698 unless up
699 do (search-backward-regexp "^* " nil t)
700 end
701 until (not (funcall (if up
702 'search-forward-regexp
703 'search-backward-regexp)
704 "^* "
705 nil
706 t))
707 finally (unless up
708 (forward-char 2)))
709 (goto-char point)
710 (unless (eobp)
711 (unless N
712 (setf N 1))
713 (unless up
714 (unless (eq (1- N) 0)
715 (setf N (- (- (forward-line (1- N)) (1- N))))))
716 (loop repeat N
717 until (ampc-move-impl up)))))
718
719 (defun ampc-toggle-state (state arg)
720 (when (or arg ampc-status)
721 (ampc-send-command
722 state
723 nil
724 (cond ((null arg)
725 (if (equal (cdr (assq state ampc-status)) "1")
726 0
727 1))
728 ((> (prefix-numeric-value arg) 0) 1)
729 (t 0)))))
730
731 (defun ampc-playlist ()
732 (ampc-with-buffer 'playlists
733 (if (search-forward-regexp "^* \\(.*\\)$" nil t)
734 (match-string 1)
735 (unless (eobp)
736 (buffer-substring-no-properties
737 (+ (line-beginning-position) 2)
738 (line-end-position))))))
739
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)
747 until (eobp)
748 do (move-beginning-of-line nil)
749 (delete-char 1)
750 (insert (if select "*" " "))
751 (setf result (ampc-next-line nil)))
752 (ampc-post-mark-change-update)
753 result)
754
755 (defun ampc-post-mark-change-update ()
756 (ecase (car ampc-type)
757 ((current-playlist playlist outputs))
758 (playlists
759 (ampc-update-playlist))
760 ((song tag)
761 (loop for w in (ampc-windows)
762 with found
763 when found
764 do (with-current-buffer (window-buffer w)
765 (when (member (car ampc-type) '(song tag))
766 (ampc-set-dirty t)))
767 end
768 if (eq w (selected-window))
769 do (setf found t)
770 end)
771 (ampc-fill-tag-song))))
772
773 (defun ampc-align-point ()
774 (unless (eobp)
775 (move-beginning-of-line nil)
776 (forward-char 2)))
777
778 (defun ampc-pad (alist)
779 (loop for (offset . data) in alist
780 with first = t
781 with current-offset = 0
782 when (<= current-offset offset)
783 when (and (not first) (eq (- offset current-offset) 0))
784 do (incf offset)
785 end
786 and concat (make-string (- offset current-offset) ? )
787 and do (setf current-offset offset)
788 else
789 concat " "
790 and do (incf current-offset)
791 end
792 concat data
793 do (setf current-offset (+ current-offset (length data))
794 first nil)))
795
796 (defun ampc-update-header ()
797 (if (eq (car ampc-type) 'status)
798 (setf header-line-format nil)
799 (setf header-line-format
800 (concat
801 (make-string (floor (fringe-columns 'left t)) ? )
802 (ecase (car ampc-type)
803 (tag
804 (concat " " (plist-get (cdr ampc-type) :tag)))
805 (playlists
806 " Playlists")
807 (t
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)
811 (car p)))))))
812 (when ampc-dirty
813 " [ Updating... ]")))))
814
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))))))
823
824 (defun ampc-update ()
825 (if ampc-status
826 (loop for b in ampc-buffers
827 do (with-current-buffer b
828 (when ampc-dirty
829 (ecase (car ampc-type)
830 (outputs
831 (ampc-send-command 'outputs))
832 (playlist
833 (ampc-update-playlist))
834 ((tag song)
835 (if (assoc (ampc-tags) ampc-internal-db)
836 (ampc-fill-tag-song)
837 (push `(,(ampc-tags) . ,(ampc-create-tree))
838 ampc-internal-db)
839 (ampc-send-command 'listallinfo)))
840 (status
841 (ampc-send-command 'status)
842 (ampc-send-command 'currentsong))
843 (playlists
844 (ampc-send-command 'listplaylists))
845 (current-playlist
846 (ampc-send-command 'playlistinfo))))))
847 (ampc-send-command 'status)
848 (ampc-send-command 'currentsong)))
849
850 (defun ampc-update-playlist ()
851 (ampc-with-buffer 'playlists
852 (if (search-forward-regexp "^\\* " nil t)
853 (ampc-send-command 'listplaylistinfo
854 nil
855 (get-text-property (point) 'data))
856 (ampc-with-buffer 'playlist
857 (delete-region (point-min) (point-max))
858 (ampc-set-dirty nil)))))
859
860 (defun ampc-send-command-impl (command)
861 (when ampc-debug
862 (message (concat "ampc: " command)))
863 (process-send-string ampc-connection (concat command "\n")))
864
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)
869 (setf ampc-yield nil
870 ampc-working-timer nil)
871 (ampc-fill-status))
872 (unless ampc-working-timer
873 (setf ampc-yield 0
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
880 (nconc (if unique
881 ampc-outstanding-commands
882 (remove command ampc-outstanding-commands))
883 `(,command))))
884
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))
889 (loop for a in
890 (cdar ampc-outstanding-commands)
891 concat " "
892 concat (cond ((integerp a)
893 (number-to-string a))
894 (t a))))))
895
896 (defun ampc-tree< (a b)
897 (not (string< (if (listp a) (car a) a) (if (listp b) (car b) b))))
898
899 (defun ampc-create-tree ()
900 (avl-tree-create 'ampc-tree<))
901
902 (defun ampc-extract (tag &optional buffer)
903 (with-current-buffer (or buffer (current-buffer))
904 (if (listp tag)
905 (ampc-extract (plist-get tag :tag))
906 (save-excursion
907 (goto-char (point-min))
908 (when (search-forward-regexp
909 (concat "^" (regexp-quote tag) ": \\(.*\\)$")
910 nil
911 t)
912 (let ((result (match-string 1)))
913 (when (equal tag "Time")
914 (setf result (ampc-transform-time result)))
915 result))))))
916
917 (defun ampc-insert (element data &optional cmp)
918 (save-excursion
919 (goto-char (point-min))
920 (ecase
921 (loop until (eobp)
922 for tp = (get-text-property (+ (point) 2) 'data)
923 finally return 'insert
924 thereis
925 (cond ((eq cmp t)
926 (let ((s (buffer-substring-no-properties
927 (+ (point) 2)
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))
933 'data
934 `(,data . ,tp)))
935 'update)
936 ((string< element s)
937 'insert))))
938 (cmp
939 (let ((r (funcall cmp data tp)))
940 (if (memq r '(update insert))
941 r
942 (forward-line (1- r))
943 nil)))
944 ((equal tp data)
945 'update)
946 (t
947 (let ((s (buffer-substring-no-properties
948 (+ (point) 2)
949 (line-end-position))))
950 (unless (string< s element)
951 'insert))))
952 do (forward-line))
953 (insert
954 (insert " ")
955 (let ((start (point)))
956 (insert element "\n")
957 (put-text-property start (point) 'data (if (eq cmp t)
958 `(,data)
959 data))))
960 (update
961 (remove-text-properties (point) (1+ (point)) '(updated))
962 (equal (buffer-substring (point) (1+ (point))) "*")))))
963
964 (defun ampc-fill-tag (trees)
965 (put-text-property (point-min) (point-max) 'data nil)
966 (loop with new-trees
967 finally return new-trees
968 for tree in trees
969 do (avl-tree-mapc (lambda (e)
970 (when (ampc-insert (car e) (cdr e) t)
971 (push (cdr e) new-trees)))
972 tree)))
973
974 (defun ampc-fill-song (trees)
975 (loop
976 for songs in trees
977 do (loop for song in songs
978 do (ampc-insert
979 (ampc-pad
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)) ""))))
983 `((,song))))))
984
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) ": ")
990 nil
991 t)
992 (move-beginning-of-line nil)
993 (1- (point))))
994 (point-max))))
995
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)
1000 w))))
1001
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
1006 (loop
1007 for i from 0
1008 while (search-forward-regexp "^file: " nil t)
1009 do (save-restriction
1010 (ampc-narrow-entry)
1011 (let ((file (ampc-extract "file"))
1012 (text
1013 (ampc-pad
1014 (loop for (tag . tag-properties) in properties
1015 collect `(,(- (or (plist-get tag-properties
1016 :offset)
1017 2)
1018 2)
1019 . ,(or (ampc-extract tag)
1020 "[Not Specified]"))))))
1021 (ampc-with-buffer 'playlist
1022 (ampc-insert text
1023 `(("file" . ,file)
1024 (index . ,i))
1025 (lambda (a b)
1026 (let ((p1 (cdr (assoc 'index a)))
1027 (p2 (cdr (assoc 'index b))))
1028 (cond ((< p1 p2) 'update)
1029 ((eq p1 p2)
1030 (if (equal (cdr (assoc "file" a))
1031 (cdr (assoc "file" b)))
1032 'update
1033 'insert))
1034 (t (- p1 p2)))))))))))))
1035
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
1040 (loop
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"))
1046 (text
1047 (ampc-pad
1048 (loop for (tag . tag-properties) in properties
1049 collect `(,(- (or (plist-get tag-properties :offset)
1050 2)
1051 2)
1052 . ,(ampc-extract tag))))))
1053 (ampc-with-buffer 'outputs
1054 (ampc-insert text `(("outputid" . ,outputid)
1055 ("outputenabled" . ,outputenabled))))))))))
1056
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
1061 (loop
1062 while (search-forward-regexp "^file: " nil t)
1063 do (save-restriction
1064 (ampc-narrow-entry)
1065 (let ((file (ampc-extract "file"))
1066 (pos (ampc-extract "Pos"))
1067 (text
1068 (ampc-pad
1069 (loop for (tag . tag-properties) in properties
1070 collect `(,(- (or (plist-get tag-properties :offset)
1071 2)
1072 2)
1073 . ,(or (ampc-extract tag)
1074 "[Not Specified]"))))))
1075 (ampc-with-buffer 'current-playlist
1076 (ampc-insert text
1077 `(("file" . ,file)
1078 ("Pos" . ,(string-to-number pos)))
1079 (lambda (a b)
1080 (let ((p1 (cdr (assoc "Pos" a)))
1081 (p2 (cdr (assoc "Pos" b))))
1082 (cond ((< p1 p2) 'insert)
1083 ((eq p1 p2)
1084 (if (equal (cdr (assoc "file" a))
1085 (cdr (assoc "file" b)))
1086 'update
1087 'insert))
1088 (t (- p1 p2)))))))))))))
1089
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))))))
1097
1098 (defun ampc-yield ()
1099 (setf ampc-yield (1+ ampc-yield))
1100 (ampc-fill-status))
1101
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)
1106 (lambda (_)
1107 (insert (ampc-status) "\n")))
1108 ampc-status)
1109 (ampc-set-dirty nil)))
1110
1111 (defun ampc-fill-tag-song ()
1112 (loop
1113 with trees = `(,(cdr (assoc (ampc-tags) ampc-internal-db)))
1114 for w in (ampc-windows)
1115 do
1116 (ampc-with-buffer w
1117 (when (member (car ampc-type) '(tag song))
1118 (if ampc-dirty
1119 (ampc-fill-skeleton w
1120 (ecase (car ampc-type)
1121 (tag (setf trees (ampc-fill-tag trees)))
1122 (song (ampc-fill-song trees))))
1123 (setf trees nil)
1124 (loop while (search-forward-regexp "^* " nil t)
1125 do (setf trees (append (get-text-property (point) 'data)
1126 trees))))))))
1127
1128 (defun* ampc-transform-time (data &aux (time (string-to-number data)))
1129 (concat (number-to-string (/ time 60))
1130 ":"
1131 (when (< (% time 60) 10)
1132 "0")
1133 (number-to-string (% time 60))))
1134
1135 (defun ampc-handle-idle ()
1136 (loop until (eobp)
1137 for subsystem = (buffer-substring (point) (line-end-position))
1138 when (string-match "^changed: \\(.*\\)$" subsystem)
1139 do (case (intern (match-string 1 subsystem))
1140 (database
1141 (setf ampc-internal-db nil)
1142 (ampc-set-dirty 'tag t)
1143 (ampc-set-dirty 'song t))
1144 (output
1145 (ampc-set-dirty 'outputs t))
1146 ((player options mixer)
1147 (setf ampc-status nil)
1148 (ampc-set-dirty 'status t))
1149 (stored_playlist
1150 (ampc-set-dirty 'playlists t)
1151 (ampc-set-dirty 'playlist t))
1152 (playlist
1153 (ampc-set-dirty 'current-playlist t)
1154 (ampc-set-dirty 'status t)))
1155 end
1156 do (forward-line))
1157 (ampc-update))
1158
1159 (defun ampc-handle-setup (status)
1160 (unless (and (string-match "^ MPD \\(.+\\)\\.\\(.+\\)\\.\\(.+\\)$"
1161 status)
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)))
1165 )
1166 (or (> version-a 0)
1167 (>= version-b 15))))
1168 (error (concat "Your version of MPD is not supported. "
1169 "ampc supports MPD (protocol version) 0.15.0 "
1170 "and later"))))
1171
1172 (defun ampc-fill-internal-db (running)
1173 (loop for origin = (and (search-forward-regexp "^file: " nil t)
1174 (line-beginning-position))
1175 then next
1176 while origin
1177 for next = (progn
1178 (forward-char)
1179 (and (search-forward-regexp "^file: " nil t)
1180 (move-beginning-of-line nil)))
1181 while next
1182 do (save-restriction
1183 (narrow-to-region origin next)
1184 (ampc-fill-internal-db-entry))
1185 (goto-char origin)
1186 (when running
1187 (delete-region origin next)
1188 (setf next origin))))
1189
1190 (defun ampc-tags ()
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)))
1195 when tag
1196 collect tag
1197 end))
1198
1199 (defun ampc-fill-internal-db-entry ()
1200 (loop
1201 with data-buffer = (current-buffer)
1202 with tree = `(nil . ,(cdr (assoc (ampc-tags) ampc-internal-db)))
1203 for w in (ampc-windows)
1204 do
1205 (with-current-buffer (window-buffer w)
1206 (ampc-set-dirty t)
1207 (ecase (car ampc-type)
1208 (tag
1209 (let* ((data (or (ampc-extract (cdr ampc-type) data-buffer)
1210 "[Not Specified]"))
1211 (member (and (cdr tree) (avl-tree-member (cdr tree) data))))
1212 (cond (member (setf tree member))
1213 ((cdr tree)
1214 (setf member `(,data . nil))
1215 (avl-tree-enter (cdr tree) member)
1216 (setf tree member))
1217 (t
1218 (setf (cdr tree) (ampc-create-tree) member`(,data . nil))
1219 (avl-tree-enter (cdr tree) member)
1220 (setf tree member)))))
1221 (song
1222 (push (loop for p in `(("file")
1223 ,@(plist-get (cdr ampc-type) :properties))
1224 for data = (ampc-extract (car p) data-buffer)
1225 when data
1226 collect `(,(car p) . ,data)
1227 end)
1228 (cdr tree))
1229 (return))))))
1230
1231 (defun ampc-handle-current-song ()
1232 (loop for k in (append ampc-status-tags '("Artist" "Title"))
1233 for s = (ampc-extract k)
1234 when s
1235 do (push `(,(intern k) . ,s) ampc-status)
1236 end)
1237 (ampc-fill-status)
1238 (run-hook-with-args ampc-status-changed-hook ampc-status))
1239
1240 (defun ampc-handle-status ()
1241 (loop for k in '("volume" "repeat" "random" "consume" "xfade" "state" "song")
1242 for v = (ampc-extract k)
1243 when v
1244 do (push `(,(intern k) . ,v) ampc-status)
1245 end)
1246 (ampc-with-buffer 'current-playlist
1247 (when ampc-highlight-current-song-mode
1248 (font-lock-fontify-region (point-min) (point-max)))))
1249
1250 (defun ampc-handle-update ()
1251 (message "Database update started"))
1252
1253 (defun ampc-handle-command (status)
1254 (cond
1255 ((eq status 'error)
1256 (pop ampc-outstanding-commands))
1257 ((eq status 'running)
1258 (case (caar ampc-outstanding-commands)
1259 (listallinfo (ampc-fill-internal-db t))))
1260 (t
1261 (case (car (pop ampc-outstanding-commands))
1262 (idle
1263 (ampc-handle-idle))
1264 (setup
1265 (ampc-handle-setup status))
1266 (currentsong
1267 (ampc-handle-current-song))
1268 (status
1269 (ampc-handle-status))
1270 (update
1271 (ampc-handle-update))
1272 (listplaylistinfo
1273 (ampc-fill-playlist))
1274 (listplaylists
1275 (ampc-fill-playlists))
1276 (playlistinfo
1277 (ampc-fill-current-playlist))
1278 (listallinfo
1279 (ampc-fill-internal-db nil))
1280 (outputs
1281 (ampc-fill-outputs)))
1282 (unless ampc-outstanding-commands
1283 (ampc-update)))))
1284
1285 (defun ampc-filter (_process string)
1286 (assert (buffer-live-p (process-buffer ampc-connection)))
1287 (with-current-buffer (process-buffer ampc-connection)
1288 (when string
1289 (when ampc-debug
1290 (message "ampc: -> %s" string))
1291 (goto-char (process-mark ampc-connection))
1292 (insert string)
1293 (set-marker (process-mark ampc-connection) (point)))
1294 (save-excursion
1295 (goto-char (point-min))
1296 (let ((success))
1297 (if (or (and (search-forward-regexp
1298 "^ACK \\[\\(.*\\)\\] {.*} \\(.*\\)\n\\'"
1299 nil
1300 t)
1301 (message "ampc command error: %s (%s)"
1302 (match-string 2)
1303 (match-string 1))
1304 t)
1305 (and (search-forward-regexp "^OK\\(.*\\)\n\\'" nil t)
1306 (setf success t)))
1307 (progn
1308 (let ((match-end (match-end 0)))
1309 (save-restriction
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))))))
1316
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
1324 ampc-buffers)
1325 collect
1326 (loop for w being the windows of f
1327 thereis (and (eq (window-buffer w)
1328 b)
1329 w))))))
1330
1331 (defun* ampc-configure-frame-1 (split &aux (split-type (car split)))
1332 (if (member split-type '(vertical horizontal))
1333 (let* ((sizes))
1334 (loop with length = (if (eq split-type 'horizontal)
1335 (window-width)
1336 (window-height))
1337 with rest = length
1338 with rest-car
1339 for subsplit in (cdr split)
1340 for s = (car subsplit)
1341 if (equal s 1.0)
1342 do (push t sizes)
1343 and do (setf rest-car sizes)
1344 else
1345 do (let ((l (if (integerp s) s (floor (* s length)))))
1346 (setf rest (- rest l))
1347 (push l sizes))
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
1352 collect s)
1353 for window on (cdr sizes)
1354 do (select-window
1355 (setf (car window)
1356 (split-window nil
1357 size
1358 (eq split-type 'horizontal)))))
1359 (setf (car sizes) first-window))
1360 (loop for subsplit in (cdr split)
1361 for window in sizes
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)
1366 end))
1367 (setf (window-dedicated-p (selected-window)) nil)
1368 (ecase split-type
1369 ((tag song)
1370 (pop-to-buffer-same-window
1371 (get-buffer-create (concat "*ampc "
1372 (or (plist-get (cdr split) :tag) "Song")
1373 "*")))
1374 (ampc-tag-song-mode))
1375 (outputs
1376 (pop-to-buffer-same-window (get-buffer-create "*ampc Outputs*"))
1377 (ampc-outputs-mode))
1378 (current-playlist
1379 (pop-to-buffer-same-window (get-buffer-create "*ampc Current Playlist*"))
1380 (ampc-current-playlist-mode)
1381 (ampc-highlight-current-song-mode 1))
1382 (playlist
1383 (pop-to-buffer-same-window (get-buffer-create "*ampc Playlist*"))
1384 (ampc-playlist-mode))
1385 (playlists
1386 (pop-to-buffer-same-window (get-buffer-create "*ampc Playlists*"))
1387 (ampc-playlists-mode))
1388 (status
1389 (pop-to-buffer-same-window (get-buffer-create "*ampc Status*"))
1390 (ampc-mode)))
1391 (destructuring-bind (&key (dedicated t) (mode-line t) &allow-other-keys)
1392 (cdr split)
1393 (setf (window-dedicated-p (selected-window)) dedicated)
1394 (unless mode-line
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))
1401 ampc-buffers)
1402 (ampc-set-dirty t)))
1403
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)
1413 else
1414 do (delete-window w)
1415 end
1416 end
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))))))
1423 (ampc-update))
1424
1425 ;;; *** interactives
1426 (defun* ampc-unmark-all (&aux buffer-read-only)
1427 "Remove all marks."
1428 (interactive)
1429 (assert (ampc-in-ampc-p))
1430 (save-excursion
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))
1435
1436 (defun ampc-trigger-update ()
1437 "Trigger a database update."
1438 (interactive)
1439 (assert (ampc-on-p))
1440 (ampc-send-command 'update))
1441
1442 (defun* ampc-toggle-marks (&aux buffer-read-only)
1443 "Toggle marks. Marked entries become unmarked, and vice versa."
1444 (interactive)
1445 (assert (ampc-in-ampc-p))
1446 (save-excursion
1447 (loop for (a . b) in '(("* " . "T ")
1448 (" " . "* ")
1449 ("T " . " "))
1450 do (goto-char (point-min))
1451 (loop while (search-forward-regexp (concat "^" (regexp-quote a))
1452 nil
1453 t)
1454 do (replace-match b nil nil))))
1455 (ampc-post-mark-change-update))
1456
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."
1461 (interactive "P")
1462 (assert (ampc-in-ampc-p))
1463 (ampc-move t arg))
1464
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."
1469 (interactive "P")
1470 (assert (ampc-in-ampc-p))
1471 (ampc-move nil arg))
1472
1473 (defun ampc-mark (&optional arg)
1474 "Mark the next ARG'th entries.
1475 ARG defaults to 1."
1476 (interactive "p")
1477 (assert (ampc-in-ampc-p))
1478 (ampc-mark-impl t arg))
1479
1480 (defun ampc-unmark (&optional arg)
1481 "Unmark the next ARG'th entries.
1482 ARG defaults to 1."
1483 (interactive "p")
1484 (assert (ampc-in-ampc-p))
1485 (ampc-mark-impl nil arg))
1486
1487 (defun ampc-increase-volume (&optional arg)
1488 "Decrease volume.
1489 With prefix argument ARG, set volume to ARG percent."
1490 (interactive "P")
1491 (assert (ampc-on-p))
1492 (ampc-set-volume arg '+))
1493
1494 (defun ampc-decrease-volume (&optional arg)
1495 "Decrease volume.
1496 With prefix argument ARG, set volume to ARG percent."
1497 (interactive "P")
1498 (assert (ampc-on-p))
1499 (ampc-set-volume arg '-))
1500
1501 (defun ampc-increase-crossfade (&optional arg)
1502 "Increase crossfade.
1503 With prefix argument ARG, set crossfading to ARG seconds."
1504 (interactive "P")
1505 (assert (ampc-on-p))
1506 (ampc-set-crossfade arg '+))
1507
1508 (defun ampc-decrease-crossfade (&optional arg)
1509 "Decrease crossfade.
1510 With prefix argument ARG, set crossfading to ARG seconds."
1511 (interactive "P")
1512 (assert (ampc-on-p))
1513 (ampc-set-crossfade arg '-))
1514
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."
1519 (interactive "P")
1520 (assert (ampc-on-p))
1521 (ampc-toggle-state 'repeat arg))
1522
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.
1527
1528 When consume is activated, each song played is removed from the playlist."
1529 (interactive "P")
1530 (assert (ampc-on-p))
1531 (ampc-toggle-state 'consume arg))
1532
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."
1537 (interactive "P")
1538 (ampc-toggle-state 'random arg))
1539
1540 (defun ampc-play-this ()
1541 "Play selected song."
1542 (interactive)
1543 (assert (ampc-in-ampc-p))
1544 (unless (eobp)
1545 (ampc-send-command 'play nil (1- (line-number-at-pos)))
1546 (ampc-send-command 'pause nil 0)))
1547
1548 (defun* ampc-toggle-play
1549 (&optional arg &aux (state (cdr-safe (assq 'state ampc-status))))
1550 "Toggle play state.
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.
1554
1555 If ARG is 4, stop player rather than pause if applicable."
1556 (interactive "P")
1557 (assert (ampc-on-p))
1558 (when state
1559 (when arg
1560 (setf arg (prefix-numeric-value arg)))
1561 (ecase (intern state)
1562 (stop
1563 (when (or (null arg) (> arg 0))
1564 (ampc-send-command
1565 'play
1566 nil
1567 (if (and (eq (car ampc-type) 'current-playlist) (not (eobp)))
1568 (1- (line-number-at-pos))
1569 0))))
1570 (pause
1571 (when (or (null arg) (> arg 0))
1572 (ampc-send-command 'pause nil 0)))
1573 (play
1574 (cond ((or (null arg) (< arg 0))
1575 (ampc-send-command 'pause nil 1))
1576 ((eq arg 4)
1577 (ampc-send-command 'stop)))))))
1578
1579 (defun ampc-next (&optional arg)
1580 "Play next song.
1581 With prefix argument ARG, skip ARG songs."
1582 (interactive "p")
1583 (assert (ampc-on-p))
1584 (ampc-skip (or arg 1)))
1585
1586 (defun ampc-previous (&optional arg)
1587 "Play previous song.
1588 With prefix argument ARG, skip ARG songs."
1589 (interactive "p")
1590 (assert (ampc-on-p))
1591 (ampc-skip (- (or arg 1))))
1592
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))
1598 (if (ampc-playlist)
1599 (ampc-send-command 'rename nil (ampc-playlist) new-name)
1600 (error "No playlist selected")))
1601
1602 (defun ampc-load ()
1603 "Load selected playlist in the current playlist."
1604 (interactive)
1605 (assert (ampc-in-ampc-p))
1606 (if (ampc-playlist)
1607 (ampc-send-command 'load nil (ampc-quote (ampc-playlist)))
1608 (error "No playlist selected")))
1609
1610 (defun ampc-toggle-output-enabled (&optional arg)
1611 "Toggle the next ARG outputs.
1612 If ARG is omitted, use the selected entries."
1613 (interactive "P")
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")
1618 'disableoutput
1619 'enableoutput)
1620 nil
1621 (cdr (assoc "outputid" data))))))
1622
1623 (defun ampc-delete (&optional arg)
1624 "Delete the next ARG songs from the playlist.
1625 If ARG is omitted, use the selected entries."
1626 (interactive "P")
1627 (assert (ampc-in-ampc-p))
1628 (let ((point (point)))
1629 (ampc-with-selection arg
1630 (let ((val (1- (- (line-number-at-pos) index))))
1631 (if (ampc-playlist)
1632 (ampc-send-command 'playlistdelete
1633 t
1634 (ampc-quote (ampc-playlist))
1635 val)
1636 (ampc-send-command 'delete t val))))
1637 (goto-char point)
1638 (ampc-align-point)))
1639
1640 (defun ampc-shuffle ()
1641 "Shuffle playlist."
1642 (interactive)
1643 (assert (ampc-on-p))
1644 (if (not (ampc-playlist))
1645 (ampc-send-command 'shuffle)
1646 (ampc-with-buffer 'playlist
1647 (let ((shuffled
1648 (mapcar
1649 'car
1650 (sort (loop until (eobp)
1651 collect `(,(cdr (assoc "file" (get-text-property
1652 (+ 2 (point))
1653 'data)))
1654 . ,(random))
1655 do (forward-line))
1656 (lambda (a b)
1657 (< (cdr a) (cdr b)))))))
1658 (ampc-clear)
1659 (loop for s in shuffled
1660 do (ampc-add-impl s))))))
1661
1662 (defun ampc-clear ()
1663 "Clear playlist."
1664 (interactive)
1665 (assert (ampc-on-p))
1666 (if (ampc-playlist)
1667 (ampc-send-command 'playlistclear nil (ampc-quote (ampc-playlist)))
1668 (ampc-send-command 'clear)))
1669
1670 (defun ampc-add (&optional arg)
1671 "Add the next ARG songs associated with the entries after point
1672 to the playlist.
1673 If ARG is omitted, use the selected entries in the current buffer."
1674 (interactive "P")
1675 (assert (ampc-in-ampc-p))
1676 (ampc-with-selection arg
1677 (ampc-add-impl)))
1678
1679 (defun ampc-status ()
1680 "Display the information that is displayed in the status window."
1681 (interactive)
1682 (assert (ampc-on-p))
1683 (let* ((flags (mapconcat
1684 'identity
1685 (loop for (f . n) in '((repeat . "Repeat")
1686 (random . "Random")
1687 (consume . "Consume"))
1688 when (equal (cdr (assq f ampc-status)) "1")
1689 collect n
1690 end)
1691 "|"))
1692 (state (cdr (assq 'state ampc-status)))
1693 (status (concat "State: " state
1694 (when ampc-yield
1695 (concat (make-string (- 10 (length state)) ? )
1696 (nth (% ampc-yield 4) '("|" "/" "-" "\\"))))
1697 "\n"
1698 (when (equal state "play")
1699 (concat "Playing: "
1700 (or (cdr-safe (assq 'Artist ampc-status))
1701 "[Not Specified]")
1702 " - "
1703 (or (cdr-safe (assq 'Title ampc-status))
1704 "[Not Specified]")
1705 "\n"))
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))
1712 status))
1713
1714 (defun ampc-delete-playlist ()
1715 "Delete selected playlist."
1716 (interactive)
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))))))
1722
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)))
1729
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."
1733 (interactive)
1734 (assert (ampc-in-ampc-p))
1735 (when song
1736 (ampc-with-buffer 'current-playlist
1737 no-se
1738 (select-window (ampc-get-window 'current-playlist))
1739 (goto-char (point-min))
1740 (forward-line (string-to-number song))
1741 (ampc-align-point))))
1742
1743 (defun ampc-previous-line (&optional arg)
1744 "Go to previous ARG'th entry in the current buffer.
1745 ARG defaults to 1."
1746 (interactive "p")
1747 (assert (ampc-in-ampc-p))
1748 (ampc-next-line (* (or arg 1) -1)))
1749
1750 (defun ampc-next-line (&optional arg)
1751 "Go to next ARG'th entry in the current buffer.
1752 ARG defaults to 1."
1753 (interactive "p")
1754 (assert (ampc-in-ampc-p))
1755 (forward-line arg)
1756 (if (eobp)
1757 (progn (forward-line -1)
1758 (forward-char 2)
1759 t)
1760 (ampc-align-point)
1761 nil))
1762
1763 (defun* ampc-suspend (&optional (run-hook t))
1764 "Suspend ampc.
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."
1768 (interactive)
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)
1774 when found-window
1775 do (delete-window w)
1776 else
1777 do (setf found-window t
1778 (window-dedicated-p w) nil)
1779 end
1780 end)
1781 (loop for b in ampc-all-buffers
1782 when (buffer-live-p b)
1783 do (kill-buffer b)
1784 end)
1785 (setf ampc-buffers nil
1786 ampc-all-buffers nil
1787 ampc-working-timer nil)
1788 (when run-hook
1789 (run-hooks 'ampc-suspend-hook)))
1790
1791 (defun ampc-quit (&optional arg)
1792 "Quit ampc.
1793 If called with a prefix argument ARG, kill the mpd instance that
1794 ampc is connected to."
1795 (interactive "P")
1796 (when (ampc-on-p)
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))
1807 (ampc-suspend nil)
1808 (setf ampc-connection nil
1809 ampc-internal-db nil
1810 ampc-outstanding-commands nil
1811 ampc-status nil)
1812 (run-hooks 'ampc-quit-hook))
1813
1814 ;;;###autoload
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.
1818
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 ""))
1826 (setf port 6600))
1827 (when (and ampc-connection
1828 (or (not (equal host ampc-host))
1829 (not (equal port ampc-port))
1830 (not (ampc-on-p))))
1831 (ampc-quit))
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)
1837 (point-max))
1838 (current-buffer))
1839 host
1840 port
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)
1846 ampc-host host
1847 ampc-port port))
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))
1855
1856 (provide 'ampc)
1857
1858 ;; Local Variables:
1859 ;; eval: (outline-minor-mode 1)
1860 ;; outline-regexp: ";;; \\*+"
1861 ;; lexical-binding: t
1862 ;; fill-column: 80
1863 ;; indent-tabs-mode: nil
1864 ;; End: