* lisp/emacs-lisp/cconv.el: New file.
[bpt/emacs.git] / lisp / mpc.el
CommitLineData
94d11cb5 1;;; -*- lexical-binding: t -*-
e1ada222
SM
2;;; mpc.el --- A client for the Music Player Daemon -*- coding: utf-8 -*-
3
73b0cd50 4;; Copyright (C) 2006-2011 Free Software Foundation, Inc.
e1ada222
SM
5
6;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
7;; Keywords: multimedia
8
9;; This file is part of GNU Emacs.
10
11;; GNU Emacs is free software: you can redistribute it and/or modify
12;; it under the terms of the GNU General Public License as published by
13;; the Free Software Foundation, either version 3 of the License, or
14;; (at your option) any later version.
15
16;; GNU Emacs is distributed in the hope that it will be useful,
17;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19;; GNU General Public License for more details.
20
21;; You should have received a copy of the GNU General Public License
22;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
23
24;;; Commentary:
25
26;; This is an Emacs front end to the Music Player Daemon.
27
28;; It mostly provides a browser inspired from Rhythmbox for your music
29;; collection and also allows you to play the music you select. The basic
30;; interface is somewhat unusual in that it does not focus on the
31;; playlist as much as on the browser.
32;; I play albums rather than songs and thus don't have much need for
33;; playlists, and it shows. Playlist support exists, but is still limited.
34
35;; Bugs:
36
37;; - when reaching end/start of song while ffwd/rewind, it may get wedged,
38;; signal an error, ... or when mpc-next/prev is called while ffwd/rewind.
39;; - MPD errors are not reported to the user.
40
41;; Todo:
42
43;; - add bindings/buttons/menuentries for the various commands.
44;; - mpc-undo
45;; - visual feedback for drag'n'drop
c710ac3c 46;; - display/set `repeat' and `random' state (and maybe also `crossfade').
e1ada222
SM
47;; - allow multiple *mpc* sessions in the same Emacs to control different mpds.
48;; - look for .folder.png (freedesktop) or folder.jpg (XP) as well.
49;; - fetch album covers and lyrics from the web?
50;; - improve MPC-Status: better volume control, add a way to show/hide the
51;; rest, plus add the buttons currently in the toolbar.
52;; - improve mpc-songs-mode's header-line column-headings so they can be
53;; dragged to resize.
54;; - allow selecting several entries by drag-mouse.
55;; - poll less often
56;; - use the `idle' command
57;; - do the time-ticking locally (and sync every once in a while)
58;; - look at the end of play time to make sure we notice the end
59;; as soon as possible
60;; - better volume widget.
61;; - add synthesized tags.
62;; e.g. pseudo-artist = artist + composer + performer.
63;; e.g. pseudo-performer = performer or artist
64;; e.g. rewrite artist "Foo bar & baz" to "Foo bar".
65;; e.g. filename regexp -> compilation flag
66;; - window/buffer management.
67;; - menubar, tooltips, ...
68;; - add mpc-describe-song, mpc-describe-album, ...
69;; - add import/export commands (especially export to an MP3 player).
70;; - add a real notion of album (as opposed to just album-name):
71;; if all songs with same album-name have same artist -> it's an album
72;; else it's either several albums or a compilation album (or both),
73;; in which case we could use heuristics or user provided info:
74;; - if the user followed the 1-album = 1-dir idea, then we can group songs
75;; by their directory to create albums.
76;; - if a `compilation' flag is available, and if <=1 of the songs have it
77;; set, then we can group songs by their artist to create albums.
78;; - if two songs have the same track-nb and disk-nb, they're not in the
79;; same album. So from the set of songs with identical album names, we
80;; can get a lower bound on the number of albums involved, and then see
81;; which of those may be non-compilations, etc...
82;; - use a special directory name for compilations.
83;; - ask the web ;-)
84
85;;; Code:
86
87;; Prefixes used in this code:
88;; mpc-proc : management of connection (in/out formatting, ...)
89;; mpc-status : auto-updated status info
90;; mpc-volume : stuff handling the volume widget
91;; mpc-cmd : mpdlib abstraction
92
93;; UI-commands : mpc-
94;; internal : mpc--
95
96(eval-when-compile (require 'cl))
97
e1ada222
SM
98(defgroup mpc ()
99 "A Client for the Music Player Daemon."
100 :prefix "mpc-"
101 :group 'multimedia
102 :group 'applications)
103
18c812bd
SM
104(defcustom mpc-browser-tags '(Genre Artist|Composer|Performer
105 Album|Playlist)
e1ada222 106 "Tags for which a browser buffer should be created by default."
18c812bd
SM
107 ;; FIXME: provide a list of tags, for completion.
108 :type '(repeat symbol))
e1ada222
SM
109
110;;; Misc utils ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
111
112(defun mpc-assq-all (key alist)
113 (let ((res ()) val)
114 (dolist (elem alist)
115 (if (and (eq (car elem) key)
116 (not (member (setq val (cdr elem)) res)))
117 (push val res)))
118 (nreverse res)))
94423e8a 119
e1ada222
SM
120(defun mpc-union (&rest lists)
121 (let ((res (nreverse (pop lists))))
122 (dolist (list lists)
123 (let ((seen res)) ;Don't remove duplicates within each list.
124 (dolist (elem list)
125 (unless (member elem seen) (push elem res)))))
126 (nreverse res)))
127
128(defun mpc-intersection (l1 l2 &optional selectfun)
129 "Return L1 after removing all elements not found in L2.
c710ac3c
JB
130If SELECTFUN is non-nil, elements aren't compared directly, but instead
131they are passed through SELECTFUN before comparison."
e1ada222
SM
132 (let ((res ()))
133 (if selectfun (setq l2 (mapcar selectfun l2)))
134 (dolist (elem l1)
135 (when (member (if selectfun (funcall selectfun elem) elem) l2)
136 (push elem res)))
137 (nreverse res)))
138
139(defun mpc-event-set-point (event)
140 (condition-case nil (posn-set-point (event-end event))
141 (error (condition-case nil (mouse-set-point event)
142 (error nil)))))
143
144(defun mpc-compare-strings (str1 str2 &optional ignore-case)
145 "Compare strings STR1 and STR2.
146Contrary to `compare-strings', this tries to get numbers sorted
147numerically rather than lexicographically."
148 (let ((res (compare-strings str1 nil nil str2 nil nil ignore-case)))
149 (if (not (integerp res)) res
150 (let ((index (1- (abs res))))
151 (if (or (>= index (length str1)) (>= index (length str2)))
152 res
153 (let ((digit1 (memq (aref str1 index)
154 '(?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9)))
155 (digit2 (memq (aref str2 index)
156 '(?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9))))
157 (if digit1
158 (if digit2
159 (let ((num1 (progn (string-match "[0-9]+" str1 index)
160 (match-string 0 str1)))
161 (num2 (progn (string-match "[0-9]+" str2 index)
162 (match-string 0 str2))))
163 (cond
164 ;; Here we presume that leading zeroes are only used
165 ;; for same-length numbers. So we'll incorrectly
166 ;; consider that "000" comes after "01", but I don't
167 ;; think it matters.
168 ((< (length num1) (length num2)) (- (abs res)))
169 ((> (length num1) (length num2)) (abs res))
170 ((< (string-to-number num1) (string-to-number num2))
171 (- (abs res)))
172 (t (abs res))))
173 ;; "1a" comes before "10", but "0" comes before "a".
174 (if (and (not (zerop index))
175 (memq (aref str1 (1- index))
176 '(?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9)))
177 (abs res)
178 (- (abs res))))
179 (if digit2
180 ;; "1a" comes before "10", but "0" comes before "a".
181 (if (and (not (zerop index))
182 (memq (aref str1 (1- index))
183 '(?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9)))
184 (- (abs res))
185 (abs res))
186 res))))))))
187
188(defun mpc-string-prefix-p (str1 str2)
189 ;; FIXME: copied from pcvs-util.el.
190 "Tell whether STR1 is a prefix of STR2."
191 (eq t (compare-strings str2 nil (length str1) str1 nil nil)))
192
193;; This can speed up mpc--song-search significantly. The table may grow
194;; very large, tho. It's only bounded by the fact that it gets flushed
195;; whenever the connection is established; which seems to work OK thanks
196;; to the fact that MPD tends to disconnect fairly often, although our
197;; constant polling often prevents disconnection.
198(defvar mpc--find-memoize (make-hash-table :test 'equal)) ;; :weakness t
199(defvar mpc-tag nil) (make-variable-buffer-local 'mpc-tag)
200
201;;; Support for the actual connection and MPD command execution ;;;;;;;;;;;;
202
203(defcustom mpc-host
204 (concat (or (getenv "MPD_HOST") "localhost")
205 (if (getenv "MPD_PORT") (concat ":" (getenv "MPD_PORT"))))
206 "Host (and port) where the Music Player Daemon is running.
207The format is \"HOST\" or \"HOST:PORT\" where PORT defaults to 6600
c710ac3c 208and HOST defaults to localhost."
e1ada222
SM
209 :type 'string)
210
211(defvar mpc-proc nil)
212
213(defconst mpc--proc-end-re "^\\(?:OK\\(?: MPD .*\\)?\\|ACK \\(.*\\)\\)\n")
214
215(put 'mpc-proc-error 'error-conditions '(mpc-proc-error error))
216(put 'mpc-proc-error 'error-message "MPD error")
217
218(defun mpc--debug (format &rest args)
219 (if (get-buffer "*MPC-debug*")
220 (with-current-buffer "*MPC-debug*"
221 (goto-char (point-max))
222 (insert-before-markers ;So it scrolls.
223 (replace-regexp-in-string "\n" "\n "
224 (apply 'format format args))
225 "\n"))))
226
227(defun mpc--proc-filter (proc string)
228 (mpc--debug "Receive \"%s\"" string)
229 (with-current-buffer (process-buffer proc)
230 (if (process-get proc 'ready)
231 (if nil ;; (string-match "\\`\\(OK\n\\)+\\'" string)
232 ;; I haven't figured out yet why I get those extraneous OKs,
233 ;; so I'll just ignore them for now.
234 nil
235 (delete-process proc)
236 (set-process-buffer proc nil)
237 (pop-to-buffer (clone-buffer))
238 (error "MPD output while idle!?"))
239 (save-excursion
240 (let ((start (or (marker-position (process-mark proc)) (point-min))))
241 (goto-char start)
242 (insert string)
243 (move-marker (process-mark proc) (point))
244 (beginning-of-line)
245 (when (and (< start (point))
246 (re-search-backward mpc--proc-end-re start t))
247 (process-put proc 'ready t)
248 (unless (eq (match-end 0) (point-max))
249 (error "Unexpected trailing text"))
250 (let ((error (match-string 1)))
251 (delete-region (point) (point-max))
252 (let ((callback (process-get proc 'callback)))
253 (process-put proc 'callback nil)
254 (if error (signal 'mpc-proc-error error))
255 (funcall callback)))))))))
256
257(defun mpc--proc-connect (host)
258 (mpc--debug "Connecting to %s..." host)
259 (with-current-buffer (get-buffer-create (format " *mpc-%s*" host))
260 ;; (pop-to-buffer (current-buffer))
261 (let (proc)
262 (while (and (setq proc (get-buffer-process (current-buffer)))
263 (progn ;; (debug)
264 (delete-process proc)))))
265 (erase-buffer)
266 (let ((port 6600))
267 (when (string-match ":[^.]+\\'" host)
268 (setq port (substring host (1+ (match-beginning 0))))
269 (setq host (substring host 0 (match-beginning 0)))
270 (unless (string-match "[^[:digit:]]" port)
271 (setq port (string-to-number port))))
272 (let* ((coding-system-for-read 'utf-8-unix)
273 (coding-system-for-write 'utf-8-unix)
274 (proc (open-network-stream "MPC" (current-buffer) host port)))
275 (when (processp mpc-proc)
276 ;; Inherit the properties of the previous connection.
277 (let ((plist (process-plist mpc-proc)))
278 (while plist (process-put proc (pop plist) (pop plist)))))
279 (mpc-proc-buffer proc 'mpd-commands (current-buffer))
280 (process-put proc 'callback 'ignore)
281 (process-put proc 'ready nil)
282 (clrhash mpc--find-memoize)
283 (set-process-filter proc 'mpc--proc-filter)
284 (set-process-sentinel proc 'ignore)
285 (set-process-query-on-exit-flag proc nil)
286 ;; This may be called within a process filter ;-(
287 (with-local-quit (mpc-proc-sync proc))
288 proc))))
289
290(defun mpc--proc-quote-string (s)
291 (if (numberp s) (number-to-string s)
292 (setq s (replace-regexp-in-string "[\"\\]" "\\\\\\&" s))
293 (if (string-match " " s) (concat "\"" s "\"") s)))
294
295(defconst mpc--proc-alist-to-alists-starters '(file directory))
296
297(defun mpc--proc-alist-to-alists (alist)
298 (assert (or (null alist)
299 (memq (caar alist) mpc--proc-alist-to-alists-starters)))
300 (let ((starter (caar alist))
301 (alists ())
302 tmp)
303 (dolist (pair alist)
304 (when (eq (car pair) starter)
305 (if tmp (push (nreverse tmp) alists))
306 (setq tmp ()))
307 (push pair tmp))
308 (if tmp (push (nreverse tmp) alists))
309 (nreverse alists)))
310
311(defun mpc-proc ()
312 (or (and mpc-proc
313 (buffer-live-p (process-buffer mpc-proc))
314 (not (memq (process-status mpc-proc) '(closed)))
315 mpc-proc)
316 (setq mpc-proc (mpc--proc-connect mpc-host))))
317
318(defun mpc-proc-sync (&optional proc)
319 "Wait for MPC process until it is idle again.
320Return the buffer in which the process is/was running."
321 (unless proc (setq proc (mpc-proc)))
322 (unwind-protect
323 (condition-case err
324 (progn
325 (while (and (not (process-get proc 'ready))
326 (accept-process-output proc)))
327 (if (process-get proc 'ready) (process-buffer proc)
328 ;; (delete-process proc)
329 (error "No response from MPD")))
330 (error (message "MPC: %s" err) (signal (car err) (cdr err))))
331 (unless (process-get proc 'ready)
332 ;; (debug)
333 (message "Killing hung process")
334 (delete-process proc))))
335
336(defun mpc-proc-cmd (cmd &optional callback)
337 "Send command CMD to the MPD server.
338If CALLBACK is nil, wait for the command to finish before returning,
339otherwise return immediately and call CALLBACK with no argument
340when the command terminates.
341CMD can be a string which is passed as-is to MPD or a list of strings
342which will be concatenated with proper quoting before passing them to MPD."
343 (let ((proc (mpc-proc)))
344 (if (and callback (not (process-get proc 'ready)))
94d11cb5 345 (let ((old (process-get proc 'callback)))
e1ada222
SM
346 (process-put proc 'callback
347 (lambda ()
348 (funcall old)
349 (mpc-proc-cmd cmd callback))))
350 ;; Wait for any pending async command to terminate.
351 (mpc-proc-sync proc)
352 (process-put proc 'ready nil)
353 (with-current-buffer (process-buffer proc)
354 (erase-buffer)
355 (mpc--debug "Send \"%s\"" cmd)
356 (process-send-string
357 proc (concat (if (stringp cmd) cmd
358 (mapconcat 'mpc--proc-quote-string cmd " "))
359 "\n")))
360 (if callback
94d11cb5 361 (let ((buf (current-buffer)))
e1ada222
SM
362 (process-put proc 'callback
363 callback
364 ;; (lambda ()
365 ;; (funcall callback
366 ;; (prog1 (current-buffer)
367 ;; (set-buffer buf))))
368 ))
369 ;; If `callback' is nil, we're executing synchronously.
370 (process-put proc 'callback 'ignore)
371 ;; This returns the process's buffer.
372 (mpc-proc-sync proc)))))
373
374;; This function doesn't exist in Emacs-21.
375;; (put 'mpc-proc-cmd-list 'byte-optimizer 'byte-optimize-pure-func)
376(defun mpc-proc-cmd-list (cmds)
377 (concat "command_list_begin\n"
378 (mapconcat (lambda (cmd)
379 (if (stringp cmd) cmd
380 (mapconcat 'mpc--proc-quote-string cmd " ")))
381 cmds
382 "\n")
383 "\ncommand_list_end"))
384
385(defun mpc-proc-cmd-list-ok ()
386 ;; To implement this, we'll need to tweak the process filter since we'd
387 ;; then sometimes get "trailing" text after "OK\n".
388 (error "Not implemented yet"))
389
390(defun mpc-proc-buf-to-alist (&optional buf)
391 (with-current-buffer (or buf (current-buffer))
392 (let ((res ()))
393 (goto-char (point-min))
394 (while (re-search-forward "^\\([^:]+\\): \\(.*\\)\n" nil t)
395 (push (cons (intern (match-string 1)) (match-string 2)) res))
396 (nreverse res))))
397
398(defun mpc-proc-buf-to-alists (buf)
399 (mpc--proc-alist-to-alists (mpc-proc-buf-to-alist buf)))
400
401(defun mpc-proc-cmd-to-alist (cmd &optional callback)
402 (if callback
94d11cb5 403 (let ((buf (current-buffer)))
e1ada222
SM
404 (mpc-proc-cmd cmd (lambda ()
405 (funcall callback (prog1 (mpc-proc-buf-to-alist
406 (current-buffer))
407 (set-buffer buf))))))
408 ;; (lexical-let ((res nil))
409 ;; (mpc-proc-cmd-to-alist cmd (lambda (alist) (setq res alist)))
410 ;; (mpc-proc-sync)
411 ;; res)
412 (mpc-proc-buf-to-alist (mpc-proc-cmd cmd))))
413
414(defun mpc-proc-tag-string-to-sym (tag)
415 (intern (capitalize tag)))
416
417(defun mpc-proc-buffer (proc use &optional buffer)
418 (let* ((bufs (process-get proc 'buffers))
419 (buf (cdr (assoc use bufs))))
420 (cond
421 ((and buffer (buffer-live-p buf) (not (eq buffer buf)))
422 (error "Duplicate MPC buffer for %s" use))
423 (buffer
424 (if buf
425 (setcdr (assoc use bufs) buffer)
426 (process-put proc 'buffers (cons (cons use buffer) bufs))))
427 (t buf))))
428
429;;; Support for regularly updated current status information ;;;;;;;;;;;;;;;
430
431;; Exported elements:
432;; `mpc-status' holds the uptodate data.
433;; `mpc-status-callbacks' holds the registered callback functions.
434;; `mpc-status-refresh' forces a refresh of the data.
435;; `mpc-status-stop' stops the automatic updating.
436
437(defvar mpc-status nil)
438(defvar mpc-status-callbacks
439 '((state . mpc--status-timers-refresh)
440 ;; (song . mpc--queue-refresh)
441 ;; (state . mpc--queue-refresh) ;To detect the end of the last song.
442 (state . mpc--faster-toggle-refresh) ;Only ffwd/rewind while play/pause.
443 (volume . mpc-volume-refresh)
444 (file . mpc-songpointer-refresh)
445 ;; The song pointer may need updating even if the file doesn't change,
446 ;; if the same song appears multiple times in a row.
447 (song . mpc-songpointer-refresh)
448 (updating_db . mpc-updated-db)
449 (updating_db . mpc--status-timers-refresh)
450 (t . mpc-current-refresh))
451 "Alist associating properties to the functions that care about them.
452Each entry has the form (PROP . FUN) where PROP can be t to mean
453to call FUN for any change whatsoever.")
454
455(defun mpc--status-callback ()
456 (let ((old-status mpc-status))
457 ;; Update the alist.
458 (setq mpc-status (mpc-proc-buf-to-alist))
459 (assert mpc-status)
460 (unless (equal old-status mpc-status)
461 ;; Run the relevant refresher functions.
462 (dolist (pair mpc-status-callbacks)
463 (when (or (eq t (car pair))
464 (not (equal (cdr (assq (car pair) old-status))
465 (cdr (assq (car pair) mpc-status)))))
466 (funcall (cdr pair)))))))
467
468(defvar mpc--status-timer nil)
469(defun mpc--status-timer-start ()
470 (add-hook 'pre-command-hook 'mpc--status-timer-stop)
471 (unless mpc--status-timer
472 (setq mpc--status-timer (run-with-timer 1 1 'mpc--status-timer-run))))
473(defun mpc--status-timer-stop ()
474 (when mpc--status-timer
475 (cancel-timer mpc--status-timer)
476 (setq mpc--status-timer nil)))
477(defun mpc--status-timer-run ()
478 (when (process-get (mpc-proc) 'ready)
479 (condition-case err
480 (with-local-quit (mpc-status-refresh))
481 (error (message "MPC: %s" err)))))
482
483(defvar mpc--status-idle-timer nil)
484(defun mpc--status-idle-timer-start ()
485 (when mpc--status-idle-timer
486 ;; Turn it off even if we'll start it again, in case it changes the delay.
487 (cancel-timer mpc--status-idle-timer))
488 (setq mpc--status-idle-timer
489 (run-with-idle-timer 1 t 'mpc--status-idle-timer-run))
490 ;; Typically, the idle timer is started from the mpc--status-callback,
491 ;; which is run asynchronously while we're already idle (we typically
492 ;; just started idling), so the timer itself will only be run the next
493 ;; time we idle :-(
494 ;; To work around that, we immediately start the repeat timer.
495 (mpc--status-timer-start))
496(defun mpc--status-idle-timer-stop (&optional really)
497 (when mpc--status-idle-timer
498 ;; Turn it off even if we'll start it again, in case it changes the delay.
499 (cancel-timer mpc--status-idle-timer))
500 (setq mpc--status-idle-timer
501 (unless really
502 ;; We don't completely stop the timer, so that if some other MPD
503 ;; client starts playback, we may get a chance to notice it.
504 (run-with-idle-timer 10 t 'mpc--status-idle-timer-run))))
505(defun mpc--status-idle-timer-run ()
506 (when (process-get (mpc-proc) 'ready)
507 (condition-case err
508 (with-local-quit (mpc-status-refresh))
509 (error (message "MPC: %s" err))))
510 (mpc--status-timer-start))
511
512(defun mpc--status-timers-refresh ()
513 "Start/stop the timers according to whether a song is playing."
514 (if (or (member (cdr (assq 'state mpc-status)) '("play"))
515 (cdr (assq 'updating_db mpc-status)))
516 (mpc--status-idle-timer-start)
517 (mpc--status-idle-timer-stop)
518 (mpc--status-timer-stop)))
519
520(defun mpc-status-refresh (&optional callback)
521 "Refresh `mpc-status'."
94d11cb5 522 (let ((cb callback))
e1ada222
SM
523 (mpc-proc-cmd (mpc-proc-cmd-list '("status" "currentsong"))
524 (lambda ()
525 (mpc--status-callback)
526 (if cb (funcall cb))))))
527
528(defun mpc-status-stop ()
529 "Stop the autorefresh of `mpc-status'.
530This is normally used only when quitting MPC.
531Any call to `mpc-status-refresh' may cause it to be restarted."
532 (setq mpc-status nil)
533 (mpc--status-idle-timer-stop 'really)
534 (mpc--status-timer-stop))
535
536;;; A thin layer above the raw protocol commands ;;;;;;;;;;;;;;;;;;;;;;;;;;;
537
538;; (defvar mpc-queue nil)
539;; (defvar mpc-queue-back nil)
540
541;; (defun mpc--queue-head ()
542;; (if (stringp (car mpc-queue)) (car mpc-queue) (cadar mpc-queue)))
543;; (defun mpc--queue-pop ()
544;; (when mpc-queue ;Can be nil if out of sync.
545;; (let ((song (car mpc-queue)))
546;; (assert song)
547;; (push (if (and (consp song) (cddr song))
548;; ;; The queue's first element is itself a list of
549;; ;; songs, where the first element isn't itself a song
550;; ;; but a description of the list.
551;; (prog1 (cadr song) (setcdr song (cddr song)))
552;; (prog1 (if (consp song) (cadr song) song)
553;; (setq mpc-queue (cdr mpc-queue))))
554;; mpc-queue-back)
555;; (assert (stringp (car mpc-queue-back))))))
556
557;; (defun mpc--queue-refresh ()
558;; ;; Maintain the queue.
559;; (mpc--debug "mpc--queue-refresh")
560;; (let ((pos (cdr (or (assq 'Pos mpc-status) (assq 'song mpc-status)))))
561;; (cond
562;; ((null pos)
563;; (mpc-cmd-clear 'ignore))
564;; ((or (not (member pos '("0" nil)))
565;; ;; There's only one song in the playlist and we've stopped.
566;; ;; Maybe it's because of some external client that set the
567;; ;; playlist like that and/or manually stopped the playback, but
568;; ;; it's more likely that we've simply reached the end of
569;; ;; the song. So remove it.
570;; (and (equal (assq 'state mpc-status) "stop")
571;; (equal (assq 'playlistlength mpc-status) "1")
572;; (setq pos "1")))
573;; ;; We're not playing the first song in the queue/playlist any
574;; ;; more, so update the queue.
575;; (dotimes (i (string-to-number pos)) (mpc--queue-pop))
576;; (mpc-proc-cmd (mpc-proc-cmd-list
577;; (make-list (string-to-number pos) "delete 0"))
578;; 'ignore)
579;; (if (not (equal (cdr (assq 'file mpc-status))
580;; (mpc--queue-head)))
581;; (message "MPC's queue is out of sync"))))))
582
18c812bd
SM
583(defvar mpc--find-memoize-union-tags nil)
584
585(defun mpc-cmd-flush (tag value)
586 (puthash (cons tag value) nil mpc--find-memoize)
587 (dolist (uniontag mpc--find-memoize-union-tags)
588 (if (member (symbol-name tag) (split-string (symbol-name uniontag) "|"))
589 (puthash (cons uniontag value) nil mpc--find-memoize))))
590
591
592(defun mpc-cmd-special-tag-p (tag)
593 (or (memq tag '(Playlist Search Directory))
594 (string-match "|" (symbol-name tag))))
595
e1ada222
SM
596(defun mpc-cmd-find (tag value)
597 "Return a list of all songs whose tag TAG has value VALUE.
598The songs are returned as alists."
599 (or (gethash (cons tag value) mpc--find-memoize)
600 (puthash (cons tag value)
601 (cond
602 ((eq tag 'Playlist)
603 ;; Special case for pseudo-tag playlist.
18c812bd
SM
604 (let ((l (condition-case err
605 (mpc-proc-buf-to-alists
606 (mpc-proc-cmd (list "listplaylistinfo" value)))
607 (mpc-proc-error
608 ;; "[50@0] {listplaylistinfo} No such playlist"
609 nil)))
e1ada222
SM
610 (i 0))
611 (mapcar (lambda (s)
612 (prog1 (cons (cons 'Pos (number-to-string i)) s)
613 (incf i)))
614 l)))
615 ((eq tag 'Search)
616 (mpc-proc-buf-to-alists
617 (mpc-proc-cmd (list "search" "any" value))))
618 ((eq tag 'Directory)
619 (let ((pairs
620 (mpc-proc-buf-to-alist
621 (mpc-proc-cmd (list "listallinfo" value)))))
622 (mpc--proc-alist-to-alists
623 ;; Strip away the `directory' entries.
624 (delq nil (mapcar (lambda (pair)
625 (if (eq (car pair) 'directory)
626 nil pair))
627 pairs)))))
18c812bd
SM
628 ((string-match "|" (symbol-name tag))
629 (add-to-list 'mpc--find-memoize-union-tags tag)
630 (let ((tag1 (intern (substring (symbol-name tag)
631 0 (match-beginning 0))))
632 (tag2 (intern (substring (symbol-name tag)
633 (match-end 0)))))
634 (mpc-union (mpc-cmd-find tag1 value)
635 (mpc-cmd-find tag2 value))))
e1ada222
SM
636 (t
637 (condition-case err
638 (mpc-proc-buf-to-alists
639 (mpc-proc-cmd (list "find" (symbol-name tag) value)))
640 (mpc-proc-error
641 ;; If `tag' is not one of the expected tags, MPD burps
642 ;; about not having the relevant table. FIXME: check
643 ;; the kind of error.
644 (error "Unknown tag %s" tag)
645 (let ((res ()))
646 (setq value (cons tag value))
647 (dolist (song (mpc-proc-buf-to-alists
648 (mpc-proc-cmd "listallinfo")))
649 (if (member value song) (push song res)))
650 res)))))
651 mpc--find-memoize)))
652
653(defun mpc-cmd-list (tag &optional other-tag value)
654 ;; FIXME: we could also provide a `mpc-cmd-list' alternative which
655 ;; doesn't take an "other-tag value" constraint but a "song-list" instead.
656 ;; That might be more efficient in some cases.
657 (cond
658 ((eq tag 'Playlist)
659 (let ((pls (mpc-assq-all 'playlist (mpc-proc-cmd-to-alist "lsinfo"))))
660 (when other-tag
661 (dolist (pl (prog1 pls (setq pls nil)))
662 (let ((plsongs (mpc-cmd-find 'Playlist pl)))
18c812bd 663 (if (not (mpc-cmd-special-tag-p other-tag))
e1ada222
SM
664 (when (member (cons other-tag value)
665 (apply 'append plsongs))
666 (push pl pls))
667 ;; Problem N°2: we compute the intersection whereas all
668 ;; we care about is whether it's empty. So we could
669 ;; speed this up significantly.
670 ;; We only compare file names, because the full song-entries
671 ;; are slightly different (the ones in plsongs include
672 ;; position and id info specific to the playlist), and it's
673 ;; good enough because this is only used with "search", which
674 ;; doesn't pay attention to playlists and URLs anyway.
675 (let* ((osongs (mpc-cmd-find other-tag value))
676 (ofiles (mpc-assq-all 'file (apply 'append osongs)))
677 (plfiles (mpc-assq-all 'file (apply 'append plsongs))))
678 (when (mpc-intersection plfiles ofiles)
679 (push pl pls)))))))
680 pls))
681
682 ((eq tag 'Directory)
683 (if (null other-tag)
684 (apply 'nconc
685 (mpc-assq-all 'directory
686 (mpc-proc-buf-to-alist
687 (mpc-proc-cmd "lsinfo")))
688 (mapcar (lambda (dir)
689 (let ((shortdir
690 (if (get-text-property 0 'display dir)
691 (concat " "
692 (get-text-property 0 'display dir))
693 " ↪ "))
694 (subdirs
695 (mpc-assq-all 'directory
696 (mpc-proc-buf-to-alist
697 (mpc-proc-cmd (list "lsinfo" dir))))))
698 (dolist (subdir subdirs)
699 (put-text-property 0 (1+ (length dir))
700 'display shortdir
701 subdir))
702 subdirs))
703 (process-get (mpc-proc) 'Directory)))
704 ;; If there's an other-tag, then just extract the dir info from the
705 ;; list of other-tag's songs.
706 (let* ((other-songs (mpc-cmd-find other-tag value))
707 (files (mpc-assq-all 'file (apply 'append other-songs)))
708 (dirs '()))
709 (dolist (file files)
710 (let ((dir (file-name-directory file)))
711 (if (and dir (setq dir (directory-file-name dir))
712 (not (equal dir (car dirs))))
713 (push dir dirs))))
714 ;; Dirs might have duplicates still.
715 (setq dirs (delete-dups dirs))
716 (let ((newdirs dirs))
717 (while newdirs
718 (let ((dir (file-name-directory (pop newdirs))))
719 (when (and dir (setq dir (directory-file-name dir))
720 (not (member dir dirs)))
721 (push dir newdirs)
722 (push dir dirs)))))
723 dirs)))
724
725 ;; The UI should not provide access to such a thing anyway currently.
726 ;; But I could imagine adding in the future a browser for the "search"
727 ;; tag, which would provide things like previous searches. Not sure how
728 ;; useful that would be tho.
729 ((eq tag 'Search) (error "Not supported"))
730
18c812bd
SM
731 ((string-match "|" (symbol-name tag))
732 (let ((tag1 (intern (substring (symbol-name tag)
733 0 (match-beginning 0))))
734 (tag2 (intern (substring (symbol-name tag)
735 (match-end 0)))))
736 (mpc-union (mpc-cmd-list tag1 other-tag value)
737 (mpc-cmd-list tag2 other-tag value))))
738
e1ada222
SM
739 ((null other-tag)
740 (condition-case nil
741 (mapcar 'cdr (mpc-proc-cmd-to-alist (list "list" (symbol-name tag))))
742 (mpc-proc-error
743 ;; If `tag' is not one of the expected tags, MPD burps about not
744 ;; having the relevant table.
745 ;; FIXME: check the kind of error.
746 (error "MPD does not know this tag %s" tag)
747 (mpc-assq-all tag (mpc-proc-cmd-to-alist "listallinfo")))))
748 (t
749 (condition-case nil
18c812bd 750 (if (mpc-cmd-special-tag-p other-tag)
e1ada222
SM
751 (signal 'mpc-proc-error "Not implemented")
752 (mapcar 'cdr
753 (mpc-proc-cmd-to-alist
754 (list "list" (symbol-name tag)
755 (symbol-name other-tag) value))))
756 (mpc-proc-error
757 ;; DAMN!! the 3-arg form of `list' is new in 0.12 !!
758 ;; FIXME: check the kind of error.
759 (let ((other-songs (mpc-cmd-find other-tag value)))
760 (mpc-assq-all tag
761 ;; Don't use `nconc' now that mpc-cmd-find may
762 ;; return a memoized result.
763 (apply 'append other-songs))))))))
764
765(defun mpc-cmd-stop (&optional callback)
766 (mpc-proc-cmd "stop" callback))
767
768(defun mpc-cmd-clear (&optional callback)
769 (mpc-proc-cmd "clear" callback)
770 ;; (setq mpc-queue-back nil mpc-queue nil)
771 )
772
773(defun mpc-cmd-pause (&optional arg callback)
774 "Pause or resume playback of the queue of songs."
94d11cb5 775 (let ((cb callback))
e1ada222
SM
776 (mpc-proc-cmd (list "pause" arg)
777 (lambda () (mpc-status-refresh) (if cb (funcall cb))))
778 (unless callback (mpc-proc-sync))))
779
780(defun mpc-cmd-status ()
781 (mpc-proc-cmd-to-alist "status"))
782
783(defun mpc-cmd-play ()
784 (mpc-proc-cmd "play")
785 (mpc-status-refresh))
786
787(defun mpc-cmd-add (files &optional playlist)
788 "Add the songs FILES to PLAYLIST.
789If PLAYLIST is t or nil or missing, use the main playlist."
790 (mpc-proc-cmd (mpc-proc-cmd-list
791 (mapcar (lambda (file)
792 (if (stringp playlist)
793 (list "playlistadd" playlist file)
794 (list "add" file)))
795 files)))
796 (if (stringp playlist)
18c812bd 797 (mpc-cmd-flush 'Playlist playlist)))
e1ada222
SM
798
799(defun mpc-cmd-delete (song-poss &optional playlist)
800 "Delete the songs at positions SONG-POSS from PLAYLIST.
801If PLAYLIST is t or nil or missing, use the main playlist."
802 (mpc-proc-cmd (mpc-proc-cmd-list
803 (mapcar (lambda (song-pos)
804 (if (stringp playlist)
805 (list "playlistdelete" playlist song-pos)
806 (list "delete" song-pos)))
807 ;; Sort them from last to first, so the renumbering
808 ;; caused by the earlier deletions don't affect
809 ;; later ones.
810 (sort song-poss '>))))
811 (if (stringp playlist)
812 (puthash (cons 'Playlist playlist) nil mpc--find-memoize)))
94423e8a 813
e1ada222
SM
814
815(defun mpc-cmd-move (song-poss dest-pos &optional playlist)
816 (let ((i 0))
817 (mpc-proc-cmd
818 (mpc-proc-cmd-list
819 (mapcar (lambda (song-pos)
820 (if (>= song-pos dest-pos)
821 ;; positions past dest-pos have been
822 ;; shifted by i.
823 (setq song-pos (+ song-pos i)))
824 (prog1 (if (stringp playlist)
825 (list "playlistmove" playlist song-pos dest-pos)
826 (list "move" song-pos dest-pos))
827 (if (< song-pos dest-pos)
828 ;; This move has shifted dest-pos by 1.
829 (decf dest-pos))
830 (incf i)))
831 ;; Sort them from last to first, so the renumbering
832 ;; caused by the earlier deletions affect
833 ;; later ones a bit less.
834 (sort song-poss '>))))
835 (if (stringp playlist)
836 (puthash (cons 'Playlist playlist) nil mpc--find-memoize))))
837
838(defun mpc-cmd-update (&optional arg callback)
94d11cb5 839 (let ((cb callback))
e1ada222
SM
840 (mpc-proc-cmd (if arg (list "update" arg) "update")
841 (lambda () (mpc-status-refresh) (if cb (funcall cb))))
842 (unless callback (mpc-proc-sync))))
843
844(defun mpc-cmd-tagtypes ()
845 (mapcar 'cdr (mpc-proc-cmd-to-alist "tagtypes")))
846
847;; This was never integrated into MPD.
848;; (defun mpc-cmd-download (file)
849;; (with-current-buffer (generate-new-buffer " *mpc download*")
850;; (set-buffer-multibyte nil)
851;; (let* ((proc (mpc-proc))
852;; (stdbuf (process-buffer proc))
853;; (markpos (marker-position (process-mark proc)))
854;; (stdcoding (process-coding-system proc)))
855;; (unwind-protect
856;; (progn
857;; (set-process-buffer proc (current-buffer))
858;; (set-process-coding-system proc 'binary (cdr stdcoding))
859;; (set-marker (process-mark proc) (point))
860;; (mpc-proc-cmd (list "download" file)))
861;; (set-process-buffer proc stdbuf)
862;; (set-marker (process-mark proc) markpos stdbuf)
863;; (set-process-coding-system proc (car stdcoding) (cdr stdcoding)))
864;; ;; The command has completed, let's decode.
865;; (goto-char (point-max))
866;; (delete-char -1) ;Delete final newline.
867;; (while (re-search-backward "^>" nil t)
868;; (delete-char 1))
869;; (current-buffer))))
870
871;;; Misc ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
872
873(defcustom mpc-mpd-music-directory nil
874 "Location of MPD's music directory."
875 :type '(choice (const nil) directory))
876
877(defcustom mpc-data-directory
878 (if (and (not (file-directory-p "~/.mpc"))
879 (file-directory-p "~/.emacs.d"))
880 "~/.emacs.d/mpc" "~/.mpc")
881 "Directory where MPC.el stores auxiliary data."
882 :type 'directory)
883
884(defun mpc-data-directory ()
885 (unless (file-directory-p mpc-data-directory)
886 (make-directory mpc-data-directory))
887 mpc-data-directory)
888
889(defun mpc-file-local-copy (file)
890 ;; Try to set mpc-mpd-music-directory.
891 (when (and (null mpc-mpd-music-directory)
892 (string-match "\\`localhost" mpc-host))
893 (let ((files '("~/.mpdconf" "/etc/mpd.conf"))
894 file)
895 (while (and files (not file))
896 (if (file-exists-p (car files)) (setq file (car files)))
897 (setq files (cdr files)))
898 (with-temp-buffer
899 (ignore-errors (insert-file-contents file))
900 (goto-char (point-min))
901 (if (re-search-forward "^music_directory[ ]+\"\\([^\"]+\\)\"")
902 (setq mpc-mpd-music-directory
903 (match-string 1))))))
904 ;; Use mpc-mpd-music-directory if applicable, or else try to use the
905 ;; `download' command, although it's never been accepted in `mpd' :-(
906 (if (and mpc-mpd-music-directory
907 (file-exists-p (expand-file-name file mpc-mpd-music-directory)))
908 (expand-file-name file mpc-mpd-music-directory)
909 ;; (let ((aux (expand-file-name (replace-regexp-in-string "[/]" "|" file)
910 ;; (mpc-data-directory))))
911 ;; (unless (file-exists-p aux)
912 ;; (condition-case err
913 ;; (with-local-quit
914 ;; (with-current-buffer (mpc-cmd-download file)
915 ;; (write-region (point-min) (point-max) aux)
916 ;; (kill-buffer (current-buffer))))
917 ;; (mpc-proc-error (message "Download error: %s" err) (setq aux nil))))
918 ;; aux)
919 ))
920
921;;; Formatter ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
922
923(defun mpc-secs-to-time (secs)
18c812bd
SM
924 ;; We could use `format-seconds', but it doesn't seem worth the trouble
925 ;; because we'd still need to check (>= secs (* 60 100)) since the special
926 ;; %z only allows us to drop the large units for small values but
927 ;; not to drop the small units for large values.
e1ada222
SM
928 (if (stringp secs) (setq secs (string-to-number secs)))
929 (if (>= secs (* 60 100)) ;More than 100 minutes.
930 (format "%dh%02d" ;"%d:%02d:%02d"
931 (/ secs 3600) (% (/ secs 60) 60)) ;; (% secs 60)
932 (format "%d:%02d" (/ secs 60) (% secs 60))))
933
934(defvar mpc-tempfiles nil)
935(defconst mpc-tempfiles-reftable (make-hash-table :weakness 'key))
936
937(defun mpc-tempfiles-clean ()
938 (let ((live ()))
939 (maphash (lambda (k v) (push v live)) mpc-tempfiles-reftable)
940 (dolist (f mpc-tempfiles)
941 (unless (member f live) (ignore-errors (delete-file f))))
942 (setq mpc-tempfiles live)))
943
944(defun mpc-tempfiles-add (key file)
945 (mpc-tempfiles-clean)
946 (puthash key file mpc-tempfiles-reftable)
947 (push file mpc-tempfiles))
948
949(defun mpc-format (format-spec info &optional hscroll)
950 "Format the INFO according to FORMAT-SPEC, inserting the result at point."
951 (let* ((pos 0)
952 (start (point))
953 (col (if hscroll (- hscroll) 0))
954 (insert (lambda (str)
955 (cond
956 ((>= col 0) (insert str))
957 (t (insert (substring str (min (length str) (- col))))))))
958 (pred nil))
959 (while (string-match "%\\(?:%\\|\\(-\\)?\\([0-9]+\\)?{\\([[:alpha:]][[:alnum:]]*\\)\\(?:-\\([^}]+\\)\\)?}\\)" format-spec pos)
960 (let ((pre-text (substring format-spec pos (match-beginning 0))))
961 (funcall insert pre-text)
962 (setq col (+ col (string-width pre-text))))
963 (setq pos (match-end 0))
964 (if (null (match-end 3))
965 (progn
966 (funcall insert "%")
967 (setq col (+ col 1)))
968 (let* ((size (match-string 2 format-spec))
969 (tag (intern (match-string 3 format-spec)))
970 (post (match-string 4 format-spec))
971 (right-align (match-end 1))
972 (text
973 (if (eq info 'self) (symbol-name tag)
974 (case tag
975 ((Time Duration)
976 (let ((time (cdr (or (assq 'time info) (assq 'Time info)))))
977 (setq pred (list nil)) ;Just assume it's never eq.
978 (when time
979 (mpc-secs-to-time (if (and (eq tag 'Duration)
980 (string-match ":" time))
981 (substring time (match-end 0))
982 time)))))
983 (Cover
984 (let* ((dir (file-name-directory (cdr (assq 'file info))))
985 (cover (concat dir "cover.jpg"))
986 (file (condition-case err
987 (mpc-file-local-copy cover)
988 (error (message "MPC: %s" err))))
989 image)
990 ;; (debug)
991 (push `(equal ',dir (file-name-directory (cdr (assq 'file info)))) pred)
992 (if (null file)
993 ;; Make sure we return something on which we can
994 ;; place the `mpc-pred' property, as
995 ;; a negative-cache. We could also use
996 ;; a default cover.
997 (progn (setq size nil) " ")
998 (if (null size) (setq image (create-image file))
999 (let ((tempfile (make-temp-file "mpc" nil ".jpg")))
1000 (call-process "convert" nil nil nil
1001 "-scale" size file tempfile)
1002 (setq image (create-image tempfile))
1003 (mpc-tempfiles-add image tempfile)))
1004 (setq size nil)
1005 (propertize dir 'display image))))
1006 (t (let ((val (cdr (assq tag info))))
1007 ;; For Streaming URLs, there's no other info
1008 ;; than the URL in `file'. Pretend it's in `Title'.
1009 (when (and (null val) (eq tag 'Title))
1010 (setq val (cdr (assq 'file info))))
1011 (push `(equal ',val (cdr (assq ',tag info))) pred)
1012 val)))))
1013 (space (when size
1014 (setq size (string-to-number size))
1015 (propertize " " 'display
1016 (list 'space :align-to (+ col size)))))
1017 (textwidth (if text (string-width text) 0))
1018 (postwidth (if post (string-width post) 0)))
1019 (when text
1020 (let ((display
1021 (if (and size
1022 (> (+ postwidth textwidth) size))
1023 ;; This doesn't even obey double-width chars :-(
1024 (propertize
1025 (if (zerop (- size postwidth 1))
1026 (substring text 0 1)
1027 (concat (substring text 0 (- size postwidth textwidth 1)) "…"))
1028 'help-echo text)
1029 text)))
1030 (when (memq tag '(Artist Album Composer)) ;FIXME: wrong list.
1031 (setq display
1032 (propertize display
1033 'mouse-face 'highlight
1034 'follow-link t
1035 'keymap `(keymap
1036 (mouse-2
1037 . (lambda ()
1038 (interactive)
1039 (mpc-constraints-push 'noerror)
1040 (mpc-constraints-restore
1041 ',(list (list tag text)))))))))
1042 (funcall insert
1043 (concat (when size
1044 (propertize " " 'display
1045 (list 'space :align-to
1046 (+ col
1047 (if (and size right-align)
1048 (- size postwidth textwidth)
1049 0)))))
1050 display post))))
1051 (if (null size) (setq col (+ col textwidth postwidth))
1052 (insert space)
1053 (setq col (+ col size))))))
1054 (put-text-property start (point) 'mpc-pred
1055 `(lambda (info) (and ,@(nreverse pred))))))
94423e8a 1056
e1ada222
SM
1057;;; The actual UI code ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1058
1059(defvar mpc-mode-map
1060 (let ((map (make-keymap)))
1061 (suppress-keymap map)
1062 ;; (define-key map "\e" 'mpc-stop)
1063 (define-key map "q" 'mpc-quit)
1064 (define-key map "\r" 'mpc-select)
1065 (define-key map [(shift return)] 'mpc-select-toggle)
1066 (define-key map [mouse-2] 'mpc-select)
1067 (define-key map [S-mouse-2] 'mpc-select-extend)
1068 (define-key map [C-mouse-2] 'mpc-select-toggle)
1069 (define-key map [drag-mouse-2] 'mpc-drag-n-drop)
1070 ;; We use `always' because a binding to t is like a binding to nil.
1071 (define-key map [follow-link] 'always)
1072 ;; Doesn't work because the first click changes the buffer, so the second
1073 ;; is applied elsewhere :-(
1074 ;; (define-key map [(double mouse-2)] 'mpc-play-at-point)
1075 (define-key map "p" 'mpc-pause)
1076 map))
1077
1078(easy-menu-define mpc-mode-menu mpc-mode-map
1079 "Menu for MPC.el."
1080 '("MPC.el"
1081 ["Add new browser" mpc-tagbrowser]
1082 ["Update DB" mpc-update]
1083 ["Quit" mpc-quit]))
1084
1085(defvar mpc-tool-bar-map
1086 (let ((map (make-sparse-keymap)))
1087 (tool-bar-local-item "mpc/prev" 'mpc-prev 'prev map
1088 :enable '(not (equal (cdr (assq 'state mpc-status)) "stop")))
1089 ;; FIXME: how can we bind it to the down-event?
1090 (tool-bar-local-item "mpc/rewind" 'mpc-rewind 'rewind map
1091 :enable '(not (equal (cdr (assq 'state mpc-status)) "stop"))
1092 :button '(:toggle . (and mpc--faster-toggle-timer
1093 (not mpc--faster-toggle-forward))))
1094 ;; We could use a single toggle command for pause/play, with 2 different
1095 ;; icons depending on whether or not it's selected, but then it'd have
1096 ;; to be a toggle-button, thus displayed depressed in one of the
1097 ;; two states :-(
1098 (tool-bar-local-item "mpc/pause" 'mpc-pause 'pause map
1099 :visible '(equal (cdr (assq 'state mpc-status)) "play")
1100 :help "Pause/play")
1101 (tool-bar-local-item "mpc/play" 'mpc-play 'play map
1102 :visible '(not (equal (cdr (assq 'state mpc-status)) "play"))
1103 :help "Play/pause")
1104 ;; FIXME: how can we bind it to the down-event?
1105 (tool-bar-local-item "mpc/ffwd" 'mpc-ffwd 'ffwd map
1106 :enable '(not (equal (cdr (assq 'state mpc-status)) "stop"))
1107 :button '(:toggle . (and mpc--faster-toggle-timer
1108 mpc--faster-toggle-forward)))
1109 (tool-bar-local-item "mpc/next" 'mpc-next 'next map
1110 :enable '(not (equal (cdr (assq 'state mpc-status)) "stop")))
1111 (tool-bar-local-item "mpc/stop" 'mpc-stop 'stop map)
1112 (tool-bar-local-item "mpc/add" 'mpc-playlist-add 'add map
1113 :help "Append to the playlist")
1114 map))
1115
1116(define-derived-mode mpc-mode fundamental-mode "MPC"
1117 "Major mode for the features common to all buffers of MPC."
1118 (buffer-disable-undo)
1119 (setq buffer-read-only t)
1120 (set (make-local-variable 'tool-bar-map) mpc-tool-bar-map)
1121 (set (make-local-variable 'truncate-lines) t))
1122
1123;;; The mpc-status-mode buffer ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1124
1125(define-derived-mode mpc-status-mode mpc-mode "MPC-Status"
1126 "Major mode to display MPC status info."
1127 (set (make-local-variable 'mode-line-format)
1128 '("%e" mode-line-frame-identification mode-line-buffer-identification))
1129 (set (make-local-variable 'window-area-factor) 3)
1130 (set (make-local-variable 'header-line-format) '("MPC " mpc-volume)))
1131
1132(defvar mpc-status-buffer-format
1133 '("%-5{Time} / %{Duration} %2{Disc--}%4{Track}" "%{Title}" "%{Album}" "%{Artist}" "%128{Cover}"))
1134
1135(defun mpc-status-buffer-refresh ()
1136 (let ((buf (mpc-proc-buffer (mpc-proc) 'status)))
1137 (when (buffer-live-p buf)
1138 (with-current-buffer buf
1139 (save-excursion
1140 (goto-char (point-min))
1141 (when (assq 'file mpc-status)
1142 (let ((inhibit-read-only t))
1143 (dolist (spec mpc-status-buffer-format)
1144 (let ((pred (get-text-property (point) 'mpc-pred)))
1145 (if (and pred (funcall pred mpc-status))
1146 (forward-line)
1147 (delete-region (point) (line-beginning-position 2))
1148 (ignore-errors (mpc-format spec mpc-status))
1149 (insert "\n"))))
1150 (unless (eobp) (delete-region (point) (point-max))))))))))
1151
1152(defun mpc-status-buffer-show ()
1153 (interactive)
1154 (let* ((buf (mpc-proc-buffer (mpc-proc) 'status))
1155 (songs-buf (mpc-proc-buffer (mpc-proc) 'songs))
1156 (songs-win (if songs-buf (get-buffer-window songs-buf 0))))
1157 (unless (buffer-live-p buf)
1158 (setq buf (get-buffer-create "*MPC-Status*"))
1159 (with-current-buffer buf
1160 (mpc-status-mode))
1161 (mpc-proc-buffer (mpc-proc) 'status buf))
1162 (if (null songs-win) (pop-to-buffer buf)
1163 (let ((win (split-window songs-win 20 t)))
1164 (set-window-dedicated-p songs-win nil)
1165 (set-window-buffer songs-win buf)
1166 (set-window-dedicated-p songs-win 'soft)))))
1167
1168;;; Selection management;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1169
1170(defvar mpc-separator-ol nil)
1171
1172(defvar mpc-select nil)
1173(make-variable-buffer-local 'mpc-select)
1174
1175(defmacro mpc-select-save (&rest body)
1176 "Execute BODY and restore the selection afterwards."
1177 (declare (indent 0) (debug t))
1178 `(let ((selection (mpc-select-get-selection))
1179 (position (cons (buffer-substring-no-properties
1180 (line-beginning-position) (line-end-position))
1181 (current-column))))
1182 ,@body
1183 (mpc-select-restore selection)
1184 (goto-char (point-min))
1185 (if (re-search-forward
1186 (concat "^" (regexp-quote (car position)) "$")
1187 (if (overlayp mpc-separator-ol)
1188 (overlay-end mpc-separator-ol))
1189 t)
1190 (move-to-column (cdr position)))
1191 (let ((win (get-buffer-window (current-buffer) 0)))
1192 (if win (set-window-point win (point))))))
1193
1194(defun mpc-select-get-selection ()
1195 (mapcar (lambda (ol)
1196 (buffer-substring-no-properties
1197 (overlay-start ol) (1- (overlay-end ol))))
1198 mpc-select))
1199
1200(defun mpc-select-restore (selection)
1201 ;; Restore the selection. I.e. move the overlays back to their
1202 ;; corresponding location. Actually which overlay is used for what
1203 ;; doesn't matter.
1204 (mapc 'delete-overlay mpc-select)
1205 (setq mpc-select nil)
1206 (dolist (elem selection)
1207 ;; After an update, some elements may have disappeared.
1208 (goto-char (point-min))
1209 (when (re-search-forward
1210 (concat "^" (regexp-quote elem) "$") nil t)
1211 (mpc-select-make-overlay)))
1212 (when mpc-tag (mpc-tagbrowser-all-select))
1213 (beginning-of-line))
1214
1215(defun mpc-select-make-overlay ()
1216 (assert (not (get-char-property (point) 'mpc-select)))
1217 (let ((ol (make-overlay
1218 (line-beginning-position) (line-beginning-position 2))))
1219 (overlay-put ol 'mpc-select t)
1220 (overlay-put ol 'face 'region)
1221 (overlay-put ol 'evaporate t)
1222 (push ol mpc-select)))
1223
1224(defun mpc-select (&optional event)
1225 "Select the tag value at point."
1226 (interactive (list last-nonmenu-event))
1227 (mpc-event-set-point event)
1228 (if (and (bolp) (eobp)) (forward-line -1))
1229 (mapc 'delete-overlay mpc-select)
1230 (setq mpc-select nil)
1231 (if (mpc-tagbrowser-all-p)
1232 nil
1233 (mpc-select-make-overlay))
1234 (when mpc-tag
1235 (mpc-tagbrowser-all-select)
1236 (mpc-selection-refresh)))
1237
1238(defun mpc-select-toggle (&optional event)
1239 "Toggle the selection of the tag value at point."
1240 (interactive (list last-nonmenu-event))
1241 (mpc-event-set-point event)
1242 (save-excursion
1243 (cond
1244 ;; The line is already selected: deselect it.
1245 ((get-char-property (point) 'mpc-select)
1246 (let ((ols nil))
1247 (dolist (ol mpc-select)
1248 (if (and (<= (overlay-start ol) (point))
1249 (> (overlay-end ol) (point)))
1250 (delete-overlay ol)
1251 (push ol ols)))
1252 (assert (= (1+ (length ols)) (length mpc-select)))
1253 (setq mpc-select ols)))
1254 ;; We're trying to select *ALL* additionally to others.
1255 ((mpc-tagbrowser-all-p) nil)
1256 ;; Select the current line.
1257 (t (mpc-select-make-overlay))))
1258 (when mpc-tag
1259 (mpc-tagbrowser-all-select)
1260 (mpc-selection-refresh)))
1261
1262(defun mpc-select-extend (&optional event)
1263 "Extend the selection up to point."
1264 (interactive (list last-nonmenu-event))
1265 (mpc-event-set-point event)
1266 (if (null mpc-select)
1267 ;; If nothing's selected yet, fallback to selecting the elem at point.
1268 (mpc-select event)
1269 (save-excursion
1270 (cond
1271 ;; The line is already in a selected area; truncate the area.
1272 ((get-char-property (point) 'mpc-select)
1273 (let ((before 0)
1274 (after 0)
1275 (mid (line-beginning-position))
1276 start end)
1277 (while (and (zerop (forward-line 1))
1278 (get-char-property (point) 'mpc-select))
1279 (setq end (1+ (point)))
1280 (incf after))
1281 (goto-char mid)
1282 (while (and (zerop (forward-line -1))
1283 (get-char-property (point) 'mpc-select))
1284 (setq start (point))
1285 (incf before))
1286 (if (and (= after 0) (= before 0))
1287 ;; Shortening an already minimum-size region: do nothing.
1288 nil
1289 (if (> after before)
1290 (setq end mid)
1291 (setq start (1+ mid)))
1292 (let ((ols '()))
1293 (dolist (ol mpc-select)
1294 (if (and (>= (overlay-start ol) start)
1295 (< (overlay-start ol) end))
1296 (delete-overlay ol)
1297 (push ol ols)))
1298 (setq mpc-select (nreverse ols))))))
1299 ;; Extending a prior area. Look for the closest selection.
1300 (t
1301 (when (mpc-tagbrowser-all-p)
1302 (forward-line 1))
1303 (let ((before 0)
1304 (count 0)
1305 (dir 1)
1306 (start (line-beginning-position)))
1307 (while (and (zerop (forward-line 1))
1308 (not (get-char-property (point) 'mpc-select)))
1309 (incf count))
1310 (unless (get-char-property (point) 'mpc-select)
1311 (setq count nil))
1312 (goto-char start)
1313 (while (and (zerop (forward-line -1))
1314 (not (get-char-property (point) 'mpc-select)))
1315 (incf before))
1316 (unless (get-char-property (point) 'mpc-select)
1317 (setq before nil))
1318 (when (and before (or (null count) (< before count)))
1319 (setq count before)
1320 (setq dir -1))
1321 (goto-char start)
1322 (dotimes (i (1+ (or count 0)))
1323 (mpc-select-make-overlay)
1324 (forward-line dir))))))
1325 (when mpc-tag
1326 (mpc-tagbrowser-all-select)
1327 (mpc-selection-refresh))))
1328
1329;;; Constraint sets ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1330
1331(defvar mpc--song-search nil)
1332
1333(defun mpc-constraints-get-current (&optional avoid-buf)
1334 "Return currently selected set of constraints.
1335If AVOID-BUF is non-nil, it specifies a buffer which should be ignored
1336when constructing the set of constraints."
1337 (let ((constraints (if mpc--song-search `((Search ,mpc--song-search))))
1338 tag select)
1339 (dolist (buf (process-get (mpc-proc) 'buffers))
1340 (setq buf (cdr buf))
1341 (when (and (setq tag (buffer-local-value 'mpc-tag buf))
1342 (not (eq buf avoid-buf))
1343 (setq select
1344 (with-current-buffer buf (mpc-select-get-selection))))
1345 (push (cons tag select) constraints)))
1346 constraints))
1347
1348(defun mpc-constraints-restore (constraints)
1349 (let ((search (assq 'Search constraints)))
1350 (setq mpc--song-search (cadr search))
1351 (when search (setq constraints (delq search constraints))))
1352 (dolist (buf (process-get (mpc-proc) 'buffers))
1353 (setq buf (cdr buf))
1354 (when (buffer-live-p buf)
1355 (let* ((tag (buffer-local-value 'mpc-tag buf))
1356 (constraint (assq tag constraints)))
1357 (when tag
1358 (with-current-buffer buf
1359 (mpc-select-restore (cdr constraint)))))))
1360 (mpc-selection-refresh))
1361
1362;; I don't get the ring.el code. I think it doesn't do what I need, but
1363;; then I don't understand when what it does would be useful.
1364(defun mpc-ring-make (size) (cons 0 (cons 0 (make-vector size nil))))
1365(defun mpc-ring-push (ring val)
1366 (aset (cddr ring) (car ring) val)
1367 (setcar (cdr ring) (max (cadr ring) (1+ (car ring))))
1368 (setcar ring (mod (1+ (car ring)) (length (cddr ring)))))
1369(defun mpc-ring-pop (ring)
1370 (setcar ring (mod (1- (car ring)) (cadr ring)))
1371 (aref (cddr ring) (car ring)))
1372
1373(defvar mpc-constraints-ring (mpc-ring-make 10))
1374
1375(defun mpc-constraints-push (&optional noerror)
1376 "Push the current selection on the ring for later."
1377 (interactive)
1378 (let ((constraints (mpc-constraints-get-current)))
1379 (if (null constraints)
1380 (unless noerror (error "No selection to push"))
1381 (mpc-ring-push mpc-constraints-ring constraints))))
1382
1383(defun mpc-constraints-pop ()
1384 "Recall the most recently pushed selection."
1385 (interactive)
1386 (let ((constraints (mpc-ring-pop mpc-constraints-ring)))
1387 (if (null constraints)
1388 (error "No selection to return to")
1389 (mpc-constraints-restore constraints))))
1390
1391;;; The TagBrowser mode ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1392
1393(defconst mpc-tagbrowser-all-name (propertize "*ALL*" 'face 'italic))
1394(defvar mpc-tagbrowser-all-ol nil)
1395(make-variable-buffer-local 'mpc-tagbrowser-all-ol)
1396(defvar mpc-tag-name nil) (make-variable-buffer-local 'mpc-tag-name)
1397(defun mpc-tagbrowser-all-p ()
1398 (and (eq (point-min) (line-beginning-position))
1399 (equal mpc-tagbrowser-all-name
1400 (buffer-substring (point-min) (line-end-position)))))
1401
1402(define-derived-mode mpc-tagbrowser-mode mpc-mode '("MPC-" mpc-tag-name)
1403 (set (make-local-variable 'mode-line-process) '("" mpc-tag-name))
1404 (set (make-local-variable 'mode-line-format) nil)
1405 (set (make-local-variable 'header-line-format) '("" mpc-tag-name ;; "s"
1406 ))
1407 (set (make-local-variable 'buffer-undo-list) t)
1408 )
1409
1410(defun mpc-tagbrowser-refresh ()
1411 (mpc-select-save
1412 (widen)
1413 (goto-char (point-min))
1414 (assert (looking-at (regexp-quote mpc-tagbrowser-all-name)))
1415 (forward-line 1)
1416 (let ((inhibit-read-only t))
1417 (delete-region (point) (point-max))
1418 (dolist (val (mpc-cmd-list mpc-tag)) (insert val "\n")))
1419 (set-buffer-modified-p nil))
1420 (mpc-reorder))
1421
1422(defun mpc-updated-db ()
1423 ;; FIXME: This is not asynchronous, but is run from a process filter.
1424 (unless (assq 'updating_db mpc-status)
1425 (clrhash mpc--find-memoize)
1426 (dolist (buf (process-get (mpc-proc) 'buffers))
1427 (setq buf (cdr buf))
1428 (when (buffer-local-value 'mpc-tag buf)
1429 (with-current-buffer buf (with-local-quit (mpc-tagbrowser-refresh)))))
1430 (with-local-quit (mpc-songs-refresh))))
1431
18c812bd
SM
1432(defun mpc-tagbrowser-tag-name (tag)
1433 (cond
1434 ((string-match "|" (symbol-name tag))
1435 (let ((tag1 (intern (substring (symbol-name tag)
1436 0 (match-beginning 0))))
1437 (tag2 (intern (substring (symbol-name tag)
1438 (match-end 0)))))
1439 (concat (mpc-tagbrowser-tag-name tag1)
1440 " | "
1441 (mpc-tagbrowser-tag-name tag2))))
1442 ((string-match "y\\'" (symbol-name tag))
1443 (concat (substring (symbol-name tag) 0 -1) "ies"))
1444 (t (concat (symbol-name tag) "s"))))
1445
e1ada222
SM
1446(defun mpc-tagbrowser-buf (tag)
1447 (let ((buf (mpc-proc-buffer (mpc-proc) tag)))
1448 (if (buffer-live-p buf) buf
1449 (setq buf (get-buffer-create (format "*MPC %ss*" tag)))
1450 (mpc-proc-buffer (mpc-proc) tag buf)
1451 (with-current-buffer buf
1452 (let ((inhibit-read-only t))
1453 (erase-buffer)
1454 (if (member tag '(Directory))
1455 (mpc-tagbrowser-dir-mode)
1456 (mpc-tagbrowser-mode))
1457 (insert mpc-tagbrowser-all-name "\n"))
1458 (forward-line -1)
1459 (setq mpc-tag tag)
18c812bd 1460 (setq mpc-tag-name (mpc-tagbrowser-tag-name tag))
e1ada222
SM
1461 (mpc-tagbrowser-all-select)
1462 (mpc-tagbrowser-refresh)
1463 buf))))
1464
1465(defvar tag-browser-tagtypes
1466 (lazy-completion-table tag-browser-tagtypes
1467 (lambda ()
1468 (append '("Playlist" "Directory")
1469 (mpc-cmd-tagtypes)))))
1470
1471(defun mpc-tagbrowser (tag)
1472 "Create a new browser for TAG."
1473 (interactive
1474 (list
1475 (let ((completion-ignore-case t))
1476 (intern
1477 (completing-read "Tag: " tag-browser-tagtypes nil 'require-match)))))
1478 (let* ((newbuf (mpc-tagbrowser-buf tag))
1479 (win (get-buffer-window newbuf 0)))
1480 (if win (select-window win)
1481 (if (with-current-buffer (window-buffer (selected-window))
1482 (derived-mode-p 'mpc-tagbrowser-mode))
1483 (setq win (selected-window))
1484 ;; Find a tagbrowser-mode buffer.
1485 (let ((buffers (process-get (mpc-proc) 'buffers))
1486 buffer)
1487 (while
1488 (and buffers
1489 (not (and (buffer-live-p (setq buffer (cdr (pop buffers))))
1490 (with-current-buffer buffer
1491 (derived-mode-p 'mpc-tagbrowser-mode))
1492 (setq win (get-buffer-window buffer 0))))))))
1493 (if (not win)
1494 (pop-to-buffer newbuf)
1495 (setq win (split-window win nil 'horiz))
1496 (set-window-buffer win newbuf)
1497 (set-window-dedicated-p win 'soft)
1498 (select-window win)
1499 (balance-windows-area)))))
1500
1501(defun mpc-tagbrowser-all-select ()
1502 "Select the special *ALL* entry if no other is selected."
1503 (if mpc-select
1504 (delete-overlay mpc-tagbrowser-all-ol)
1505 (save-excursion
1506 (goto-char (point-min))
1507 (if mpc-tagbrowser-all-ol
1508 (move-overlay mpc-tagbrowser-all-ol
1509 (point) (line-beginning-position 2))
1510 (let ((ol (make-overlay (point) (line-beginning-position 2))))
1511 (overlay-put ol 'face 'region)
1512 (overlay-put ol 'evaporate t)
1513 (set (make-local-variable 'mpc-tagbrowser-all-ol) ol))))))
94423e8a 1514
e1ada222
SM
1515;; (defvar mpc-constraints nil)
1516(defun mpc-separator (active)
1517 ;; Place a separator mark.
1518 (unless mpc-separator-ol
1519 (set (make-local-variable 'mpc-separator-ol)
1520 (make-overlay (point) (point)))
1521 (overlay-put mpc-separator-ol 'after-string
1522 (propertize "\n"
1523 'face '(:height 0.05 :inverse-video t))))
1524 (goto-char (point-min))
1525 (forward-line 1)
1526 (while
1527 (and (member (buffer-substring-no-properties
1528 (line-beginning-position) (line-end-position))
1529 active)
1530 (zerop (forward-line 1))))
1531 (if (or (eobp) (null active))
1532 (delete-overlay mpc-separator-ol)
1533 (move-overlay mpc-separator-ol (1- (point)) (point))))
1534
1535(defun mpc-sort (active)
1536 ;; Sort the active elements at the front.
1537 (let ((inhibit-read-only t))
1538 (goto-char (point-min))
1539 (if (mpc-tagbrowser-all-p) (forward-line 1))
1540 (condition-case nil
1541 (sort-subr nil 'forward-line 'end-of-line
1542 nil nil
1543 (lambda (s1 s2)
1544 (setq s1 (buffer-substring-no-properties
1545 (car s1) (cdr s1)))
1546 (setq s2 (buffer-substring-no-properties
1547 (car s2) (cdr s2)))
1548 (cond
1549 ((member s1 active)
1550 (if (member s2 active)
1551 (let ((cmp (mpc-compare-strings s1 s2 t)))
1552 (and (numberp cmp) (< cmp 0)))
1553 t))
1554 ((member s2 active) nil)
1555 (t (let ((cmp (mpc-compare-strings s1 s2 t)))
1556 (and (numberp cmp) (< cmp 0)))))))
1557 ;; The comparison predicate arg is new in Emacs-22.
1558 (wrong-number-of-arguments
1559 (sort-subr nil 'forward-line 'end-of-line
1560 (lambda ()
1561 (let ((name (buffer-substring-no-properties
1562 (point) (line-end-position))))
1563 (cond
1564 ((member name active) (concat "1" name))
1565 (t (concat "2" "name"))))))))))
94423e8a 1566
e1ada222
SM
1567(defvar mpc--changed-selection)
1568
1569(defun mpc-reorder (&optional nodeactivate)
1570 "Reorder entries based on thre currently active selections.
1571I.e. split the current browser buffer into a first part containing the
1572entries included in the selection, then a separator, and then the entries
1573not included in the selection.
1574Return non-nil if a selection was deactivated."
1575 (mpc-select-save
1576 (let ((constraints (mpc-constraints-get-current (current-buffer)))
1577 (active 'all))
1578 ;; (unless (equal constraints mpc-constraints)
1579 ;; (set (make-local-variable 'mpc-constraints) constraints)
1580 (dolist (cst constraints)
1581 (let ((vals (apply 'mpc-union
1582 (mapcar (lambda (val)
1583 (mpc-cmd-list mpc-tag (car cst) val))
1584 (cdr cst)))))
1585 (setq active
1586 (if (listp active) (mpc-intersection active vals) vals))))
94423e8a 1587
e1ada222
SM
1588 (when (and (listp active))
1589 ;; Remove the selections if they are all in conflict with
1590 ;; other constraints.
1591 (let ((deactivate t))
1592 (dolist (sel selection)
1593 (when (member sel active) (setq deactivate nil)))
1594 (when deactivate
1595 ;; Variable declared/used by `mpc-select-save'.
1596 (when selection
1597 (setq mpc--changed-selection t))
1598 (unless nodeactivate
1599 (setq selection nil)
1600 (mapc 'delete-overlay mpc-select)
1601 (setq mpc-select nil)
1602 (mpc-tagbrowser-all-select)))))
1603
1604 ;; FIXME: This `mpc-sort' takes a lot of time. Maybe we should
1605 ;; be more clever and presume the buffer is mostly sorted already.
1606 (mpc-sort (if (listp active) active))
1607 (mpc-separator (if (listp active) active)))))
1608
1609(defun mpc-selection-refresh ()
1610 (let ((mpc--changed-selection t))
1611 (while mpc--changed-selection
1612 (setq mpc--changed-selection nil)
1613 (dolist (buf (process-get (mpc-proc) 'buffers))
1614 (setq buf (cdr buf))
1615 (when (and (buffer-local-value 'mpc-tag buf)
1616 (not (eq buf (current-buffer))))
1617 (with-current-buffer buf (mpc-reorder)))))
1618 ;; FIXME: reorder the current buffer last and prevent deactivation,
1619 ;; since whatever selection we made here is the most recent one
1620 ;; and should hence take precedence.
1621 (when mpc-tag (mpc-reorder 'nodeactivate))
1622 ;; FIXME: comment?
1623 (if (and mpc--song-search mpc--changed-selection)
1624 (progn
1625 (setq mpc--song-search nil)
1626 (mpc-selection-refresh))
1627 (mpc-songs-refresh))))
1628
1629;;; Hierarchical tagbrowser ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1630;; Todo:
1631;; - Add a button on each dir to open/close it (?)
1632;; - add the parent dir on the previous line, greyed-out, if it's not
1633;; present (because we're in the non-selected part and the parent is
1634;; in the selected part).
1635
1636(defvar mpc-tagbrowser-dir-mode-map
1637 (let ((map (make-sparse-keymap)))
1638 (set-keymap-parent map mpc-tagbrowser-mode-map)
1639 (define-key map [?\M-\C-m] 'mpc-tagbrowser-dir-toggle)
1640 map))
1641
1642;; (defvar mpc-tagbrowser-dir-keywords
1643;; '(mpc-tagbrowser-dir-hide-prefix))
1644
1645(define-derived-mode mpc-tagbrowser-dir-mode mpc-tagbrowser-mode '("MPC-" mpc-tag-name)
1646 ;; (set (make-local-variable 'font-lock-defaults)
1647 ;; '(mpc-tagbrowser-dir-keywords t))
1648 )
1649
1650;; (defun mpc-tagbrowser-dir-hide-prefix (limit)
1651;; (while
1652;; (let ((prev (buffer-substring (line-beginning-position 0)
1653;; (line-end-position 0))))
1654;; (
1655
1656(defun mpc-tagbrowser-dir-toggle (event)
1657 "Open or close the element at point."
1658 (interactive (list last-nonmenu-event))
1659 (mpc-event-set-point event)
1660 (let ((name (buffer-substring (line-beginning-position)
1661 (line-end-position)))
1662 (prop (intern mpc-tag)))
1663 (if (not (member name (process-get (mpc-proc) prop)))
1664 (process-put (mpc-proc) prop
1665 (cons name (process-get (mpc-proc) prop)))
1666 (let ((new (delete name (process-get (mpc-proc) prop))))
1667 (setq name (concat name "/"))
1668 (process-put (mpc-proc) prop
1669 (delq nil
1670 (mapcar (lambda (x)
1671 (if (mpc-string-prefix-p name x)
1672 nil x))
1673 new)))))
1674 (mpc-tagbrowser-refresh)))
94423e8a 1675
e1ada222
SM
1676
1677;;; Playlist management ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1678
1679(defvar mpc-songs-playlist nil
1680 "Name of the currently selected playlist, if any.
c710ac3c 1681A value of t means the main playlist.")
e1ada222
SM
1682(make-variable-buffer-local 'mpc-songs-playlist)
1683
1684(defun mpc-playlist-create (name)
1685 "Save current playlist under name NAME."
1686 (interactive "sPlaylist name: ")
1687 (mpc-proc-cmd (list "save" name))
1688 (let ((buf (mpc-proc-buffer (mpc-proc) 'Playlist)))
1689 (when (buffer-live-p buf)
1690 (with-current-buffer buf (mpc-tagbrowser-refresh)))))
1691
1692(defun mpc-playlist-destroy (name)
1693 "Delete playlist named NAME."
1694 (interactive
1695 (list (completing-read "Delete playlist: " (mpc-cmd-list 'Playlist)
1696 nil 'require-match)))
1697 (mpc-proc-cmd (list "rm" name))
1698 (let ((buf (mpc-proc-buffer (mpc-proc) 'Playlist)))
1699 (when (buffer-live-p buf)
1700 (with-current-buffer buf (mpc-tagbrowser-refresh)))))
1701
1702(defun mpc-playlist-rename (oldname newname)
1703 "Rename playlist OLDNAME to NEWNAME."
1704 (interactive
1705 (let* ((oldname (if (and (eq mpc-tag 'Playlist) (null current-prefix-arg))
1706 (buffer-substring (line-beginning-position)
1707 (line-end-position))
1708 (completing-read "Rename playlist: "
1709 (mpc-cmd-list 'Playlist)
1710 nil 'require-match)))
1711 (newname (read-string (format "Rename '%s' to: " oldname))))
1712 (if (zerop (length newname))
1713 (error "Aborted")
1714 (list oldname newname))))
1715 (mpc-proc-cmd (list "rename" oldname newname))
1716 (let ((buf (mpc-proc-buffer (mpc-proc) 'Playlist)))
1717 (if (buffer-live-p buf)
1718 (with-current-buffer buf (mpc-tagbrowser-refresh)))))
1719
1720(defun mpc-playlist ()
1721 "Show the current playlist."
1722 (interactive)
1723 (mpc-constraints-push 'noerror)
1724 (mpc-constraints-restore '()))
1725
1726(defun mpc-playlist-add ()
1727 "Add the selection to the playlist."
1728 (interactive)
1729 (let ((songs (mapcar #'car (mpc-songs-selection))))
1730 (mpc-cmd-add songs)
1731 (message "Appended %d songs" (length songs))
1732 ;; Return the songs added. Used in `mpc-play'.
1733 songs))
1734
1735(defun mpc-playlist-delete ()
1736 "Remove the selected songs from the playlist."
1737 (interactive)
1738 (unless mpc-songs-playlist
0472835f 1739 (error "The selected songs aren't part of a playlist"))
e1ada222
SM
1740 (let ((song-poss (mapcar #'cdr (mpc-songs-selection))))
1741 (mpc-cmd-delete song-poss mpc-songs-playlist)
1742 (mpc-songs-refresh)
1743 (message "Deleted %d songs" (length song-poss))))
1744
1745;;; Volume management ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1746
1747(defvar mpc-volume-map
1748 (let ((map (make-sparse-keymap)))
1749 (define-key map [down-mouse-1] 'mpc-volume-mouse-set)
1750 (define-key map [mouse-1] 'ignore)
1751 (define-key map [header-line down-mouse-1] 'mpc-volume-mouse-set)
1752 (define-key map [header-line mouse-1] 'ignore)
1753 (define-key map [mode-line down-mouse-1] 'mpc-volume-mouse-set)
1754 (define-key map [mode-line mouse-1] 'ignore)
1755 map))
1756
1757(defvar mpc-volume nil) (put 'mpc-volume 'risky-local-variable t)
1758
1759(defun mpc-volume-refresh ()
1760 ;; Maintain the volume.
1761 (setq mpc-volume
1762 (mpc-volume-widget
1763 (string-to-number (cdr (assq 'volume mpc-status))))))
1764
1765(defvar mpc-volume-step 5)
1766
1767(defun mpc-volume-mouse-set (&optional event)
1768 "Change volume setting."
1769 (interactive (list last-nonmenu-event))
1770 (let* ((posn (event-start event))
1771 (diff
1772 (if (memq (if (stringp (car-safe (posn-object posn)))
1773 (aref (car (posn-object posn)) (cdr (posn-object posn)))
1774 (with-current-buffer (window-buffer (posn-window posn))
1775 (char-after (posn-point posn))))
1776 '(?◁ ?<))
1777 (- mpc-volume-step) mpc-volume-step))
1778 (newvol (+ (string-to-number (cdr (assq 'volume mpc-status))) diff)))
1779 (mpc-proc-cmd (list "setvol" newvol) 'mpc-status-refresh)
1780 (message "Set MPD volume to %s%%" newvol)))
1781
1782(defun mpc-volume-widget (vol &optional size)
1783 (unless size (setq size 12.5))
1784 (let ((scaledvol (* (/ vol 100.0) size)))
1785 ;; (message "Volume sizes: %s - %s" (/ vol fact) (/ (- 100 vol) fact))
1786 (list (propertize "<" ;; "◁"
1787 ;; 'face 'default
1788 'keymap mpc-volume-map
1789 'face '(:box (:line-width -2 :style pressed-button))
1790 'mouse-face '(:box (:line-width -2 :style released-button)))
1791 " "
1792 (propertize "a"
1793 'display (list 'space :width scaledvol)
1794 'face '(:inverse-video t
1795 :box (:line-width -2 :style released-button)))
1796 (propertize "a"
1797 'display (list 'space :width (- size scaledvol))
1798 'face '(:box (:line-width -2 :style released-button)))
1799 " "
1800 (propertize ">" ;; "▷"
1801 ;; 'face 'default
1802 'keymap mpc-volume-map
1803 'face '(:box (:line-width -2 :style pressed-button))
1804 'mouse-face '(:box (:line-width -2 :style released-button))))))
1805
1806;;; MPC songs mode ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
1807
1808(defvar mpc-current-song nil) (put 'mpc-current-song 'risky-local-variable t)
1809(defvar mpc-current-updating nil) (put 'mpc-current-updating 'risky-local-variable t)
1810(defvar mpc-songs-format-description nil) (put 'mpc-songs-format-description 'risky-local-variable t)
1811
1812(defvar mpc-previous-window-config nil)
1813
1814(defvar mpc-songs-mode-map
1815 (let ((map (make-sparse-keymap)))
1816 (set-keymap-parent map mpc-mode-map)
1817 (define-key map [remap mpc-select] 'mpc-songs-jump-to)
1818 map))
1819
1820(defvar mpc-songpointer-set-visible nil)
1821
1822(defvar mpc-songs-hashcons (make-hash-table :test 'equal :weakness t)
1823 "Make song file name objects unique via hash consing.
1824This is used so that they can be compared with `eq', which is needed for
1825`text-property-any'.")
1826(defun mpc-songs-hashcons (name)
1827 (or (gethash name mpc-songs-hashcons) (puthash name name mpc-songs-hashcons)))
1828(defcustom mpc-songs-format "%2{Disc--}%3{Track} %-5{Time} %25{Title} %20{Album} %20{Artist} %10{Date}"
1829 "Format used to display each song in the list of songs."
1830 :type 'string)
1831
1832(defvar mpc-songs-totaltime)
1833
1834(defun mpc-songs-refresh ()
1835 (let ((buf (mpc-proc-buffer (mpc-proc) 'songs)))
1836 (when (buffer-live-p buf)
1837 (with-current-buffer buf
1838 (let ((constraints (mpc-constraints-get-current (current-buffer)))
1839 (dontsort nil)
1840 (inhibit-read-only t)
1841 (totaltime 0)
1842 (curline (cons (count-lines (point-min)
1843 (line-beginning-position))
1844 (buffer-substring (line-beginning-position)
1845 (line-end-position))))
1846 active)
1847 (setq mpc-songs-playlist nil)
1848 (if (null constraints)
1849 ;; When there are no constraints, rather than show the list of
1850 ;; all songs (which could take a while to download and
1851 ;; format), we show the current playlist.
1852 ;; FIXME: it would be good to be able to show the complete
1853 ;; list, but that would probably require us to format it
1854 ;; on-the-fly to make it bearable.
1855 (setq dontsort t
1856 mpc-songs-playlist t
1857 active (mpc-proc-buf-to-alists
1858 (mpc-proc-cmd "playlistinfo")))
1859 (dolist (cst constraints)
1860 (if (and (eq (car cst) 'Playlist)
1861 (= 1 (length (cdr cst))))
1862 (setq mpc-songs-playlist (cadr cst)))
1863 ;; We don't do anything really special here for playlists,
1864 ;; because it's unclear what's a correct "union" of playlists.
1865 (let ((vals (apply 'mpc-union
1866 (mapcar (lambda (val)
1867 (mpc-cmd-find (car cst) val))
1868 (cdr cst)))))
18c812bd
SM
1869 (setq active (cond
1870 ((null active)
e1ada222
SM
1871 (if (eq (car cst) 'Playlist)
1872 (setq dontsort t))
1873 vals)
18c812bd 1874 ((or dontsort
e1ada222
SM
1875 ;; Try to preserve ordering and
1876 ;; repetitions from playlists.
1877 (not (eq (car cst) 'Playlist)))
1878 (mpc-intersection active vals
18c812bd
SM
1879 (lambda (x) (assq 'file x))))
1880 (t
e1ada222
SM
1881 (setq dontsort t)
1882 (mpc-intersection vals active
18c812bd
SM
1883 (lambda (x)
1884 (assq 'file x)))))))))
e1ada222
SM
1885 (mpc-select-save
1886 (erase-buffer)
1887 ;; Sorting songs is surprisingly difficult: when comparing two
1888 ;; songs with the same album name but different artist name, you
1889 ;; have to know whether these are two different albums (with the
1890 ;; same name) or a single album (typically a compilation).
1891 ;; I punt on it and just use file-name sorting, which does the
1892 ;; right thing if your library is properly arranged.
1893 (dolist (song (if dontsort active
1894 (sort active
1895 (lambda (song1 song2)
1896 (let ((cmp (mpc-compare-strings
1897 (cdr (assq 'file song1))
1898 (cdr (assq 'file song2)))))
1899 (and (integerp cmp) (< cmp 0)))))))
1900 (incf totaltime (string-to-number (or (cdr (assq 'Time song)) "0")))
1901 (mpc-format mpc-songs-format song)
1902 (delete-char (- (skip-chars-backward " "))) ;Remove trailing space.
1903 (insert "\n")
1904 (put-text-property
1905 (line-beginning-position 0) (line-beginning-position)
1906 'mpc-file (mpc-songs-hashcons (cdr (assq 'file song))))
1907 (let ((pos (assq 'Pos song)))
1908 (if pos
1909 (put-text-property
1910 (line-beginning-position 0) (line-beginning-position)
1911 'mpc-file-pos (string-to-number (cdr pos)))))
1912 ))
1913 (goto-char (point-min))
1914 (forward-line (car curline))
18c812bd 1915 (if (or (search-forward (cdr curline) nil t)
e1ada222 1916 (search-backward (cdr curline) nil t))
18c812bd
SM
1917 (beginning-of-line)
1918 (goto-char (point-min)))
e1ada222
SM
1919 (set (make-local-variable 'mpc-songs-totaltime)
1920 (unless (zerop totaltime)
1921 (list " " (mpc-secs-to-time totaltime))))
1922 ))))
1923 (let ((mpc-songpointer-set-visible t))
1924 (mpc-songpointer-refresh)))
1925
1926(defun mpc-songs-search (string)
1927 "Filter songs to those who include STRING in their metadata."
1928 (interactive "sSearch for: ")
1929 (setq mpc--song-search
1930 (if (zerop (length string)) nil string))
1931 (let ((mpc--changed-selection t))
1932 (while mpc--changed-selection
1933 (setq mpc--changed-selection nil)
1934 (dolist (buf (process-get (mpc-proc) 'buffers))
1935 (setq buf (cdr buf))
1936 (when (buffer-local-value 'mpc-tag buf)
1937 (with-current-buffer buf (mpc-reorder))))
1938 (mpc-songs-refresh))))
94423e8a 1939
e1ada222
SM
1940(defun mpc-songs-kill-search ()
1941 "Turn off the current search restriction."
1942 (interactive)
1943 (mpc-songs-search nil))
1944
1945(defun mpc-songs-selection ()
1946 "Return the list of songs currently selected."
1947 (let ((buf (mpc-proc-buffer (mpc-proc) 'songs)))
1948 (when (buffer-live-p buf)
1949 (with-current-buffer buf
1950 (save-excursion
1951 (let ((files ()))
1952 (if mpc-select
1953 (dolist (ol mpc-select)
1954 (push (cons
1955 (get-text-property (overlay-start ol) 'mpc-file)
1956 (get-text-property (overlay-start ol) 'mpc-file-pos))
1957 files))
1958 (goto-char (point-min))
1959 (while (not (eobp))
1960 (push (cons
1961 (get-text-property (point) 'mpc-file)
1962 (get-text-property (point) 'mpc-file-pos))
1963 files)
1964 (forward-line 1)))
1965 (nreverse files)))))))
1966
1967(defun mpc-songs-jump-to (song-file &optional posn)
c710ac3c 1968 "Jump to song SONG-FILE; interactively, this is the song at point."
e1ada222
SM
1969 (interactive
1970 (let* ((event last-nonmenu-event)
1971 (posn (event-end event)))
1972 (with-selected-window (posn-window posn)
1973 (goto-char (posn-point posn))
1974 (list (get-text-property (point) 'mpc-file)
1975 posn))))
1976 (let* ((plbuf (mpc-proc-cmd "playlist"))
1977 (re (concat "^\\([0-9]+\\):" (regexp-quote song-file) "$"))
1978 (sn (with-current-buffer plbuf
1979 (goto-char (point-min))
1980 (when (re-search-forward re nil t)
1981 (match-string 1)))))
1982 (cond
1983 ((null sn) (error "This song is not in the playlist"))
1984 ((null (with-current-buffer plbuf (re-search-forward re nil t)))
1985 ;; song-file only appears once in the playlist: no ambiguity,
1986 ;; we're good to go!
1987 (mpc-proc-cmd (list "play" sn)))
1988 (t
1989 ;; The song appears multiple times in the playlist. If the current
1990 ;; buffer holds not only the destination song but also the current
1991 ;; song, then we will move in the playlist to the same relative
1992 ;; position as in the buffer. Otherwise, we will simply choose the
1993 ;; song occurrence closest to the current song.
1994 (with-selected-window (posn-window posn)
1995 (let* ((cur (and (markerp overlay-arrow-position)
1996 (marker-position overlay-arrow-position)))
1997 (dest (save-excursion
1998 (goto-char (posn-point posn))
1999 (line-beginning-position)))
2000 (lines (when cur (* (if (< cur dest) 1 -1)
2001 (count-lines cur dest)))))
2002 (with-current-buffer plbuf
2003 (goto-char (point-min))
2004 ;; Start the search from the current song.
2005 (forward-line (string-to-number
2006 (or (cdr (assq 'song mpc-status)) "0")))
2007 ;; If the current song is also displayed in the buffer,
2008 ;; then try to move to the same relative position.
2009 (if lines (forward-line lines))
2010 ;; Now search the closest occurrence.
2011 (let* ((next (save-excursion
2012 (when (re-search-forward re nil t)
2013 (cons (point) (match-string 1)))))
2014 (prev (save-excursion
2015 (when (re-search-backward re nil t)
2016 (cons (point) (match-string 1)))))
2017 (sn (cdr (if (and next prev)
2018 (if (< (- (car next) (point))
2019 (- (point) (car prev)))
2020 next prev)
2021 (or next prev)))))
2022 (assert sn)
2023 (mpc-proc-cmd (concat "play " sn))))))))))
2024
2025(define-derived-mode mpc-songs-mode mpc-mode "MPC-song"
2026 (setq mpc-songs-format-description
2027 (with-temp-buffer (mpc-format mpc-songs-format 'self) (buffer-string)))
2028 (set (make-local-variable 'header-line-format)
2029 ;; '("MPC " mpc-volume " " mpc-current-song)
2030 (list (propertize " " 'display '(space :align-to 0))
2031 ;; 'mpc-songs-format-description
2032 '(:eval
2033 (let ((hscroll (window-hscroll)))
2034 (with-temp-buffer
2035 (mpc-format mpc-songs-format 'self hscroll)
2036 ;; That would be simpler than the hscroll handling in
2037 ;; mpc-format, but currently move-to-column does not
2038 ;; recognize :space display properties.
2039 ;; (move-to-column hscroll)
2040 ;; (delete-region (point-min) (point))
2041 (buffer-string))))))
2042 (set (make-local-variable 'mode-line-format)
2043 '("%e" mode-line-frame-identification mode-line-buffer-identification
2044 #(" " 0 3
2045 (help-echo "mouse-1: Select (drag to resize)\nmouse-2: Make current window occupy the whole frame\nmouse-3: Remove current window from display"))
2046 mode-line-position
2047 #(" " 0 2
2048 (help-echo "mouse-1: Select (drag to resize)\nmouse-2: Make current window occupy the whole frame\nmouse-3: Remove current window from display"))
2049 mpc-songs-totaltime
2050 mpc-current-updating
2051 #(" " 0 2
2052 (help-echo "mouse-1: Select (drag to resize)\nmouse-2: Make current window occupy the whole frame\nmouse-3: Remove current window from display"))
2053 (mpc--song-search
2054 (:propertize
2055 ("Search=\"" mpc--song-search "\"")
2056 help-echo "mouse-2: kill this search"
2057 follow-link t
2058 mouse-face mode-line-highlight
2059 keymap (keymap (mode-line keymap
2060 (mouse-2 . mpc-songs-kill-search))))
2061 (:propertize "NoSearch"
2062 help-echo "mouse-2: set a search restriction"
2063 follow-link t
2064 mouse-face mode-line-highlight
2065 keymap (keymap (mode-line keymap (mouse-2 . mpc-songs-search)))))))
94423e8a 2066
e1ada222
SM
2067 ;; (set (make-local-variable 'mode-line-process)
2068 ;; '("" ;; mpc-volume " "
2069 ;; mpc-songs-totaltime
2070 ;; mpc-current-updating))
2071 )
2072
2073(defun mpc-songpointer-set (pos)
2074 (let* ((win (get-buffer-window (current-buffer) t))
2075 (visible (when win
2076 (or mpc-songpointer-set-visible
2077 (and (markerp overlay-arrow-position)
2078 (eq (marker-buffer overlay-arrow-position)
2079 (current-buffer))
2080 (<= (window-start win) overlay-arrow-position)
2081 (< overlay-arrow-position (window-end win)))))))
2082 (unless (local-variable-p 'overlay-arrow-position)
2083 (set (make-local-variable 'overlay-arrow-position) (make-marker)))
2084 (move-marker overlay-arrow-position pos)
2085 ;; If the arrow was visible, try to keep it that way.
2086 (if (and visible pos
2087 (or (> (window-start win) pos) (>= pos (window-end win t))))
2088 (set-window-point win pos))))
2089
2090(defun mpc-songpointer-refresh ()
2091 (let ((buf (mpc-proc-buffer (mpc-proc) 'songs)))
2092 (when (buffer-live-p buf)
2093 (with-current-buffer buf
2094 (let* ((pos (text-property-any
2095 (point-min) (point-max)
2096 'mpc-file (mpc-songs-hashcons
2097 (cdr (assq 'file mpc-status)))))
2098 (other (when pos
2099 (save-excursion
2100 (goto-char pos)
2101 (text-property-any
2102 (line-beginning-position 2) (point-max)
2103 'mpc-file (mpc-songs-hashcons
2104 (cdr (assq 'file mpc-status))))))))
2105 (if other
2106 ;; The song appears multiple times in the buffer.
2107 ;; We need to be careful to choose the right occurrence.
2108 (mpc-proc-cmd "playlist" 'mpc-songpointer-refresh-hairy)
2109 (mpc-songpointer-set pos)))))))
2110
2111(defun mpc-songpointer-context (size plbuf)
2112 (with-current-buffer plbuf
2113 (goto-char (point-min))
2114 (forward-line (string-to-number (or (cdr (assq 'song mpc-status)) "0")))
2115 (let ((context-before '())
2116 (context-after '()))
2117 (save-excursion
2118 (dotimes (i size)
2119 (when (re-search-backward "^[0-9]+:\\(.*\\)" nil t)
2120 (push (mpc-songs-hashcons (match-string 1)) context-before))))
2121 ;; Skip the actual current song.
2122 (forward-line 1)
2123 (dotimes (i size)
2124 (when (re-search-forward "^[0-9]+:\\(.*\\)" nil t)
2125 (push (mpc-songs-hashcons (match-string 1)) context-after)))
2126 ;; If there isn't `size' context, then return nil.
2127 (unless (and (< (length context-before) size)
2128 (< (length context-after) size))
2129 (cons (nreverse context-before) (nreverse context-after))))))
2130
2131(defun mpc-songpointer-score (context pos)
2132 (let ((count 0))
2133 (goto-char pos)
2134 (dolist (song (car context))
2135 (and (zerop (forward-line -1))
2136 (eq (get-text-property (point) 'mpc-file) song)
2137 (incf count)))
2138 (goto-char pos)
2139 (dolist (song (cdr context))
2140 (and (zerop (forward-line 1))
2141 (eq (get-text-property (point) 'mpc-file) song)
2142 (incf count)))
2143 count))
2144
2145(defun mpc-songpointer-refresh-hairy ()
2146 ;; Based on the complete playlist, we should figure out where in the
2147 ;; song buffer is the currently playing song.
2148 (let ((plbuf (current-buffer))
2149 (buf (mpc-proc-buffer (mpc-proc) 'songs)))
2150 (when (buffer-live-p buf)
2151 (with-current-buffer buf
2152 (let* ((context-size 0)
2153 (context '(() . ()))
2154 (pos (text-property-any
2155 (point-min) (point-max)
2156 'mpc-file (mpc-songs-hashcons
2157 (cdr (assq 'file mpc-status)))))
2158 (score 0)
2159 (other pos))
2160 (while
2161 (setq other
2162 (save-excursion
2163 (goto-char other)
2164 (text-property-any
2165 (line-beginning-position 2) (point-max)
2166 'mpc-file (mpc-songs-hashcons
2167 (cdr (assq 'file mpc-status))))))
2168 ;; There is an `other' contestant.
2169 (let ((other-score (mpc-songpointer-score context other)))
2170 (cond
2171 ;; `other' is worse: try the next one.
2172 ((< other-score score) nil)
2173 ;; `other' is better: remember it and then search further.
2174 ((> other-score score)
2175 (setq pos other)
2176 (setq score other-score))
2177 ;; Both are equal and increasing the context size won't help.
2178 ;; Arbitrarily choose one of the two and keep looking
2179 ;; for a better match.
2180 ((< score context-size) nil)
2181 (t
2182 ;; Score is equal and increasing context might help: try it.
2183 (incf context-size)
2184 (let ((new-context
2185 (mpc-songpointer-context context-size plbuf)))
2186 (if (null new-context)
2187 ;; There isn't more context: choose one arbitrarily
2188 ;; and keep looking for a better match elsewhere.
2189 (decf context-size)
2190 (setq context new-context)
2191 (setq score (mpc-songpointer-score context pos))
2192 (save-excursion
2193 (goto-char other)
2194 ;; Go back one line so we find `other' again.
2195 (setq other (line-beginning-position 0)))))))))
2196 (mpc-songpointer-set pos))))))
2197
2198(defun mpc-current-refresh ()
2199 ;; Maintain the current data.
2200 (mpc-status-buffer-refresh)
2201 (setq mpc-current-updating
2202 (if (assq 'updating_db mpc-status) " Updating-DB"))
2203 (ignore-errors
2204 (setq mpc-current-song
2205 (when (assq 'file mpc-status)
2206 (concat " "
2207 (mpc-secs-to-time (cdr (assq 'time mpc-status)))
2208 " "
2209 (cdr (assq 'Title mpc-status))
2210 " ("
2211 (cdr (assq 'Artist mpc-status))
2212 " / "
2213 (cdr (assq 'Album mpc-status))
2214 ")"))))
2215 (force-mode-line-update t))
2216
2217(defun mpc-songs-buf ()
2218 (let ((buf (mpc-proc-buffer (mpc-proc) 'songs)))
2219 (if (buffer-live-p buf) buf
2220 (with-current-buffer (setq buf (get-buffer-create "*MPC-Songs*"))
2221 (mpc-proc-buffer (mpc-proc) 'songs buf)
2222 (mpc-songs-mode)
2223 buf))))
2224
2225(defun mpc-update ()
2226 "Tell MPD to refresh its database."
2227 (interactive)
2228 (mpc-cmd-update))
2229
2230(defun mpc-quit ()
2231 "Quit Music Player Daemon."
2232 (interactive)
2233 (let* ((proc mpc-proc)
2234 (bufs (mapcar 'cdr (if proc (process-get proc 'buffers))))
2235 (wins (mapcar (lambda (buf) (get-buffer-window buf 0)) bufs))
2236 (song-buf (mpc-songs-buf))
2237 frames)
2238 ;; Collect all the frames where MPC buffers appear.
2239 (dolist (win wins)
2240 (when (and win (not (memq (window-frame win) frames)))
2241 (push (window-frame win) frames)))
2242 (if (and frames song-buf
2243 (with-current-buffer song-buf mpc-previous-window-config))
2244 (progn
2245 (select-frame (car frames))
2246 (set-window-configuration
2247 (with-current-buffer song-buf mpc-previous-window-config)))
2248 ;; Now delete the ones that show nothing else than MPC buffers.
2249 (dolist (frame frames)
2250 (let ((delete t))
2251 (dolist (win (window-list frame))
2252 (unless (memq (window-buffer win) bufs) (setq delete nil)))
2253 (if delete (ignore-errors (delete-frame frame))))))
2254 ;; Then kill the buffers.
2255 (mapc 'kill-buffer bufs)
2256 (mpc-status-stop)
2257 (if proc (delete-process proc))))
94423e8a 2258
e1ada222
SM
2259(defun mpc-stop ()
2260 "Stop playing the current queue of songs."
2261 (interactive)
2262 (mpc-cmd-stop)
2263 (mpc-cmd-clear)
2264 (mpc-status-refresh))
2265
2266(defun mpc-pause ()
2267 "Pause playing."
2268 (interactive)
2269 (mpc-cmd-pause "1"))
2270
2271(defun mpc-resume ()
c710ac3c 2272 "Resume playing."
e1ada222
SM
2273 (interactive)
2274 (mpc-cmd-pause "0"))
2275
2276(defun mpc-play ()
2277 "Start playing whatever is selected."
2278 (interactive)
2279 (if (member (cdr (assq 'state (mpc-cmd-status))) '("pause"))
2280 (mpc-resume)
2281 ;; When playing the playlist ends, the playlist isn't cleared, but the
2282 ;; user probably doesn't want to re-listen to it before getting to
2283 ;; listen to what he just selected.
2284 ;; (if (member (cdr (assq 'state (mpc-cmd-status))) '("stop"))
2285 ;; (mpc-cmd-clear))
2286 ;; Actually, we don't use mpc-play to append to the playlist any more,
2287 ;; so we can just always empty the playlist.
2288 (mpc-cmd-clear)
2289 (if (mpc-playlist-add)
2290 (if (member (cdr (assq 'state (mpc-cmd-status))) '("stop"))
2291 (mpc-cmd-play))
2292 (error "Don't know what to play"))))
2293
2294(defun mpc-next ()
2295 "Jump to the next song in the queue."
2296 (interactive)
2297 (mpc-proc-cmd "next")
2298 (mpc-status-refresh))
2299
2300(defun mpc-prev ()
2301 "Jump to the beginning of the current song, or to the previous song."
2302 (interactive)
2303 (let ((time (cdr (assq 'time mpc-status))))
2304 ;; Here we rely on the fact that string-to-number silently ignores
2305 ;; everything after a non-digit char.
2306 (cond
2307 ;; Go back to the beginning of current song.
2308 ((and time (> (string-to-number time) 0))
2309 (mpc-proc-cmd (list "seekid" (cdr (assq 'songid mpc-status)) 0)))
2310 ;; We're at the beginning of the first song of the playlist.
2311 ;; Fetch the previous one from `mpc-queue-back'.
2312 ;; ((and (zerop (string-to-number (cdr (assq 'song mpc-status))))
2313 ;; mpc-queue-back)
2314 ;; ;; Because we use cmd-list rather than cmd-play, the queue is not
2315 ;; ;; automatically updated.
2316 ;; (let ((prev (pop mpc-queue-back)))
2317 ;; (push prev mpc-queue)
2318 ;; (mpc-proc-cmd
2319 ;; (mpc-proc-cmd-list
2320 ;; (list (list "add" prev)
2321 ;; (list "move" (cdr (assq 'playlistlength mpc-status)) "0")
2322 ;; "previous")))))
2323 ;; We're at the beginning of a song, but not the first one.
2324 (t (mpc-proc-cmd "previous")))
2325 (mpc-status-refresh)))
2326
2327(defvar mpc-last-seek-time '(0 . 0))
2328
2329(defun mpc--faster (event speedup step)
2330 "Fast forward."
2331 (interactive (list last-nonmenu-event))
2332 (let ((repeat-delay (/ (abs (float step)) speedup)))
2333 (if (not (memq 'down (event-modifiers event)))
2334 (let* ((currenttime (float-time))
2335 (last-time (- currenttime (car mpc-last-seek-time))))
2336 (if (< last-time (* 0.9 repeat-delay))
2337 nil ;; Trottle
2338 (let* ((status (if (< last-time 1.0)
2339 mpc-status (mpc-cmd-status)))
2340 (songid (cdr (assq 'songid status)))
2341 (time (if songid
2342 (if (< last-time 1.0)
2343 (cdr mpc-last-seek-time)
2344 (string-to-number
2345 (cdr (assq 'time status)))))))
2346 (setq mpc-last-seek-time
2347 (cons currenttime (setq time (+ time step))))
2348 (mpc-proc-cmd (list "seekid" songid time)
2349 'mpc-status-refresh))))
2350 (let ((status (mpc-cmd-status)))
94d11cb5 2351 (let* ((songid (cdr (assq 'songid status)))
e1ada222
SM
2352 (time (if songid (string-to-number
2353 (cdr (assq 'time status))))))
2354 (let ((timer (run-with-timer
2355 t repeat-delay
2356 (lambda ()
2357 (mpc-proc-cmd (list "seekid" songid
2358 (setq time (+ time step)))
2359 'mpc-status-refresh)))))
2360 (while (mouse-movement-p
2361 (event-basic-type (setq event (read-event)))))
2362 (cancel-timer timer)))))))
2363
2364(defvar mpc--faster-toggle-timer nil)
2365(defun mpc--faster-stop ()
2366 (when mpc--faster-toggle-timer
2367 (cancel-timer mpc--faster-toggle-timer)
2368 (setq mpc--faster-toggle-timer nil)))
2369
2370(defun mpc--faster-toggle-refresh ()
2371 (if (equal (cdr (assq 'state mpc-status)) "stop")
2372 (mpc--faster-stop)))
2373
2374(defun mpc--songduration ()
2375 (string-to-number
2376 (let ((s (cdr (assq 'time mpc-status))))
2377 (if (not (string-match ":" s))
2378 (error "Unexpected time format %S" s)
2379 (substring s (match-end 0))))))
2380
2381(defvar mpc--faster-toggle-forward nil)
2382(defvar mpc--faster-acceleration 0.5)
2383(defun mpc--faster-toggle (speedup step)
2384 (setq speedup (float speedup))
2385 (if mpc--faster-toggle-timer
2386 (mpc--faster-stop)
2387 (mpc-status-refresh) (mpc-proc-sync)
94d11cb5
IK
2388 (let* (songid ;The ID of the currently ffwd/rewinding song.
2389 songnb ;The position of that song in the playlist.
2390 songduration ;The duration of that song.
2391 songtime ;The time of the song last time we ran.
2392 oldtime ;The timeoftheday last time we ran.
2393 prevsongid) ;The song we're in the process leaving.
e1ada222
SM
2394 (let ((fun
2395 (lambda ()
2396 (let ((newsongid (cdr (assq 'songid mpc-status)))
2397 (newsongnb (cdr (assq 'song mpc-status))))
94423e8a 2398
e1ada222
SM
2399 (if (and (equal prevsongid newsongid)
2400 (not (equal prevsongid songid)))
2401 ;; We left prevsongid and came back to it. Pretend it
2402 ;; didn't happen.
2403 (setq newsongid songid))
94423e8a 2404
e1ada222
SM
2405 (cond
2406 ((null newsongid) (mpc--faster-stop))
2407 ((not (equal songid newsongid))
2408 ;; We jumped to another song: reset.
2409 (setq songid newsongid)
2410 (setq songtime (string-to-number
2411 (cdr (assq 'time mpc-status))))
2412 (setq songduration (mpc--songduration))
2413 (setq oldtime (float-time)))
2414 ((and (>= songtime songduration) mpc--faster-toggle-forward)
2415 ;; Skip to the beginning of the next song.
2416 (if (not (equal (cdr (assq 'state mpc-status)) "play"))
2417 (mpc-proc-cmd "next" 'mpc-status-refresh)
2418 ;; If we're playing, this is done automatically, so we
2419 ;; don't need to do anything, or rather we *shouldn't*
2420 ;; do anything otherwise there's a race condition where
2421 ;; we could skip straight to the next next song.
2422 nil))
2423 ((and (<= songtime 0) (not mpc--faster-toggle-forward))
2424 ;; Skip to the end of the previous song.
2425 (setq prevsongid songid)
2426 (mpc-proc-cmd "previous"
2427 (lambda ()
2428 (mpc-status-refresh
2429 (lambda ()
2430 (setq songid (cdr (assq 'songid mpc-status)))
2431 (setq songtime (setq songduration (mpc--songduration)))
2432 (setq oldtime (float-time))
2433 (mpc-proc-cmd (list "seekid" songid songtime)))))))
2434 (t
2435 (setq speedup (+ speedup mpc--faster-acceleration))
2436 (let ((newstep
2437 (truncate (* speedup (- (float-time) oldtime)))))
2438 (if (<= newstep 1) (setq newstep 1))
2439 (setq oldtime (+ oldtime (/ newstep speedup)))
2440 (if (not mpc--faster-toggle-forward)
2441 (setq newstep (- newstep)))
2442 (setq songtime (min songduration (+ songtime newstep)))
2443 (unless (>= songtime songduration)
2444 (condition-case nil
2445 (mpc-proc-cmd
2446 (list "seekid" songid songtime)
2447 'mpc-status-refresh)
2448 (mpc-proc-error (mpc-status-refresh)))))))
2449 (setq songnb newsongnb)))))
2450 (setq mpc--faster-toggle-forward (> step 0))
2451 (funcall fun) ;Initialize values.
2452 (setq mpc--faster-toggle-timer
2453 (run-with-timer t 0.3 fun))))))
2454
2455
2456
2457(defvar mpc-faster-speedup 8)
2458
2459(defun mpc-ffwd (event)
2460 "Fast forward."
2461 (interactive (list last-nonmenu-event))
2462 ;; (mpc--faster event 4.0 1)
2463 (mpc--faster-toggle mpc-faster-speedup 1))
94423e8a 2464
e1ada222
SM
2465(defun mpc-rewind (event)
2466 "Fast rewind."
2467 (interactive (list last-nonmenu-event))
2468 ;; (mpc--faster event 4.0 -1)
2469 (mpc--faster-toggle mpc-faster-speedup -1))
94423e8a
CY
2470
2471
e1ada222
SM
2472(defun mpc-play-at-point (&optional event)
2473 (interactive (list last-nonmenu-event))
2474 (mpc-select event)
2475 (mpc-play))
2476
2477;; (defun mpc-play-tagval ()
2478;; "Play all the songs of the tag at point."
2479;; (interactive)
2480;; (let* ((val (buffer-substring (line-beginning-position) (line-end-position)))
2481;; (songs (mapcar 'cdar
2482;; (mpc-proc-buf-to-alists
2483;; (mpc-proc-cmd (list "find" mpc-tag val))))))
2484;; (mpc-cmd-add songs)
2485;; (if (member (cdr (assq 'state (mpc-cmd-status))) '("stop"))
2486;; (mpc-cmd-play))))
2487
2488;;; Drag'n'drop support ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2489;; Todo:
2490;; the main thing to do here, is to provide visual feedback during the drag:
2491;; - change the mouse-cursor.
2492;; - highlight/select the source and the current destination.
2493
2494(defun mpc-drag-n-drop (event)
2495 "DWIM for a drag EVENT."
2496 (interactive "e")
2497 (let* ((start (event-start event))
2498 (end (event-end event))
2499 (start-buf (window-buffer (posn-window start)))
2500 (end-buf (window-buffer (posn-window end)))
2501 (songs
2502 (with-current-buffer start-buf
2503 (goto-char (posn-point start))
2504 (if (get-text-property (point) 'mpc-select)
2505 ;; FIXME: actually we should only consider the constraints
2506 ;; corresponding to the selection in this particular buffer.
2507 (mpc-songs-selection)
2508 (cond
2509 ((and (derived-mode-p 'mpc-songs-mode)
2510 (get-text-property (point) 'mpc-file))
2511 (list (cons (get-text-property (point) 'mpc-file)
2512 (get-text-property (point) 'mpc-file-pos))))
2513 ((and mpc-tag (not (mpc-tagbrowser-all-p)))
2514 (mapcar (lambda (song)
2515 (list (cdr (assq 'file song))))
2516 (mpc-cmd-find
2517 mpc-tag
2518 (buffer-substring (line-beginning-position)
2519 (line-end-position)))))
2520 (t
2521 (error "Unsupported starting position for drag'n'drop gesture")))))))
2522 (with-current-buffer end-buf
2523 (goto-char (posn-point end))
2524 (cond
2525 ((eq mpc-tag 'Playlist)
2526 ;; Adding elements to a named playlist.
2527 (let ((playlist (if (or (mpc-tagbrowser-all-p)
2528 (and (bolp) (eolp)))
2529 (error "Not a playlist")
2530 (buffer-substring (line-beginning-position)
2531 (line-end-position)))))
2532 (mpc-cmd-add (mapcar 'car songs) playlist)
2533 (message "Added %d songs to %s" (length songs) playlist)
2534 (if (member playlist
2535 (cdr (assq 'Playlist (mpc-constraints-get-current))))
2536 (mpc-songs-refresh))))
2537 ((derived-mode-p 'mpc-songs-mode)
2538 (cond
2539 ((null mpc-songs-playlist)
2540 (error "The songs shown do not belong to a playlist"))
2541 ((eq start-buf end-buf)
2542 ;; Moving songs within the shown playlist.
2543 (let ((dest-pos (get-text-property (point) 'mpc-file-pos)))
2544 (mpc-cmd-move (mapcar 'cdr songs) dest-pos mpc-songs-playlist)
2545 (message "Moved %d songs" (length songs))))
2546 (t
2547 ;; Adding songs to the shown playlist.
2548 (let ((dest-pos (get-text-property (point) 'mpc-file-pos))
2549 (pl (if (stringp mpc-songs-playlist)
2550 (mpc-cmd-find 'Playlist mpc-songs-playlist)
2551 (mpc-proc-cmd-to-alist "playlist"))))
2552 ;; MPD's protocol does not let us add songs at a particular
2553 ;; position in a playlist, so we first have to add them to the
2554 ;; end, and then move them to their final destination.
2555 (mpc-cmd-add (mapcar 'car songs) mpc-songs-playlist)
2556 (mpc-cmd-move (let ((poss '()))
2557 (dotimes (i (length songs))
2558 (push (+ i (length pl)) poss))
2559 (nreverse poss)) dest-pos mpc-songs-playlist)
2560 (message "Added %d songs" (length songs)))))
2561 (mpc-songs-refresh))
2562 (t
2563 (error "Unsupported drag'n'drop gesture"))))))
2564
2565;;; Toplevel ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2566
2567(defcustom mpc-frame-alist '((name . "MPC") (tool-bar-lines . 1)
2568 (font . "Sans"))
2569 "Alist of frame parameters for the MPC frame."
2570 :type 'alist)
2571
2572;;;###autoload
2573(defun mpc ()
2574 "Main entry point for MPC."
2575 (interactive
2576 (progn
2577 (if current-prefix-arg
2578 (setq mpc-host (read-string "MPD host and port: " nil nil mpc-host)))
2579 nil))
2580 (let* ((song-buf (mpc-songs-buf))
2581 (song-win (get-buffer-window song-buf 0)))
2582 (if song-win
2583 (select-window song-win)
2584 (if (or (window-dedicated-p (selected-window))
2585 (window-minibuffer-p))
2586 (ignore-errors (select-frame (make-frame mpc-frame-alist)))
2587 (with-current-buffer song-buf
2588 (set (make-local-variable 'mpc-previous-window-config)
2589 (current-window-configuration))))
2590 (let* ((win1 (selected-window))
2591 (win2 (split-window))
2592 (tags mpc-browser-tags))
2593 (unless tags (error "Need at least one entry in `mpc-browser-tags'"))
2594 (set-window-buffer win2 song-buf)
2595 (set-window-dedicated-p win2 'soft)
2596 (mpc-status-buffer-show)
2597 (while
2598 (progn
2599 (set-window-buffer win1 (mpc-tagbrowser-buf (pop tags)))
2600 (set-window-dedicated-p win1 'soft)
2601 tags)
2602 (setq win1 (split-window win1 nil 'horiz)))))
2603 (balance-windows-area))
2604 (mpc-songs-refresh)
2605 (mpc-status-refresh))
2606
2607(provide 'mpc)
2608
e1ada222 2609;;; mpc.el ends here