1;;; emms-playlist-mode.el --- Playlist mode for Emms.
2
3;; Copyright (C) 2005, 2006, 2007, 2008, 2009  Free Software Foundation, Inc.
4
5;; Author: Yoni Rabkin <yrk@gnu.org>
6
7;; This file is part of EMMS.
8
9;; EMMS is free software; you can redistribute it and/or modify
10;; it under the terms of the GNU General Public License as published by
11;; the Free Software Foundation; either version 3, or (at your option)
12;; any later version.
13;;
14;; EMMS is distributed in the hope that it will be useful,
15;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17;; GNU General Public License for more details.
18;;
19;; You should have received a copy of the GNU General Public License
20;; along with EMMS; if not, write to the Free Software Foundation,
21;; Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
22
23;;; Commentary:
24;;;
25;;; This is a method of displaying and manipulating the different Emms
26;;; playlist buffers.
27;;;
28;;; Emms developer's motto: "When forcer says (require 'jump) we say
29;;; (funcall #'jump height)"
30
31;;; Code:
32
33;;; --------------------------------------------------------
34;;; Variables
35;;; --------------------------------------------------------
36
37(require 'emms)
38(condition-case nil
39    (require 'overlay)
40  (error nil))
41(require 'emms-source-playlist)
42
43(defvar emms-playlist-mode-hook nil
44  "Emms playlist mode hook.")
45
46(defvar emms-playlist-mode-selected-overlay nil
47  "Last selected track.  Use for updating the display.")
48
49(defvar emms-playlist-mode-switched-buffer nil
50  "Last buffer visited before calling `emms-playlist-mode-switch-buffer'.")
51
52(defvar emms-playlist-mode-popup-enabled nil
53  "True when the playlist was called as a popup window.")
54
55(defvar emms-playlist-mode-kill-whole-line-p t
56  "When true line kills behave like a typical music player.")
57
58(make-variable-buffer-local
59 'emms-playlist-mode-selected-overlay)
60
61(defgroup emms-playlist-mode nil
62  "*The Emacs Multimedia System playlist mode."
63  :prefix "emms-playlist-mode-"
64  :group 'emms)
65
66(defcustom emms-playlist-mode-open-playlists nil
67  "*Determine whether to open playlists in a new EMMS buffer on RET.
68This is useful if you have a master playlist buffer that is
69composed of other playlists."
70  :type 'boolean
71  :group 'emms-playlist-mode)
72
73(defcustom emms-playlist-mode-window-width 25
74  "*Determine the width of the Emms popup window.
75The value should a positive integer."
76  :type 'integer
77  :group 'emms-playlist-mode)
78
79(defcustom emms-playlist-mode-center-when-go nil
80  "*Determine whether to center on the currently selected track.
81This is true for every invocation of `emms-playlist-mode-go'."
82  :type 'boolean
83  :group 'emms-playlist-mode)
84
85;;; --------------------------------------------------------
86;;; Faces
87;;; --------------------------------------------------------
88
89(defface emms-playlist-track-face
90  '((((class color) (background dark))
91     (:foreground "DarkSeaGreen"))
92    (((class color) (background light))
93     (:foreground "Blue"))
94    (((type tty) (class mono))
95     (:inverse-video t))
96    (t (:background "Blue")))
97  "Face for the tracks in a playlist buffer."
98  :group 'emms-playlist-mode)
99
100(defface emms-playlist-selected-face
101  '((((class color) (background dark))
102     (:foreground "SteelBlue3"))
103    (((class color) (background light))
104     (:background "blue3" :foreground "white"))
105    (((type tty) (class mono))
106     (:inverse-video t))
107    (t (:background "blue3")))
108  "Face for highlighting the selected track."
109  :group 'emms-playlist-mode)
110
111;;; --------------------------------------------------------
112;;; Keys
113;;; --------------------------------------------------------
114
115(defvar emms-playlist-mode-map
116  (let ((map (make-sparse-keymap)))
117    (set-keymap-parent map text-mode-map)
118    (define-key map (kbd "C-x C-s") 'emms-playlist-save)
119    (define-key map (kbd "C-y") 'emms-playlist-mode-yank)
120    (define-key map (kbd "C-k") 'emms-playlist-mode-kill-track)
121    (define-key map (kbd "C-w") 'emms-playlist-mode-kill)
122    (define-key map (kbd "C-_") 'emms-playlist-mode-undo)
123    (define-key map (kbd "C-/") 'emms-playlist-mode-undo)
124    (define-key map (kbd "C-x u") 'emms-playlist-mode-undo)
125    (define-key map (kbd "C-n") 'next-line)
126    (define-key map (kbd "C-p") 'previous-line)
127    (define-key map (kbd "C-j") 'emms-playlist-mode-insert-newline)
128    (define-key map (kbd "M-y") 'emms-playlist-mode-yank-pop)
129    (define-key map (kbd "M-<") 'emms-playlist-mode-first)
130    (define-key map (kbd "M->") 'emms-playlist-mode-last)
131    (define-key map (kbd "M-n") 'emms-playlist-mode-next)
132    (define-key map (kbd "M-p") 'emms-playlist-mode-previous)
133    (define-key map (kbd "a") 'emms-playlist-mode-add-contents)
134    (define-key map (kbd "b") 'emms-playlist-set-playlist-buffer)
135    (define-key map (kbd "D") 'emms-playlist-mode-kill-track)
136    (define-key map (kbd "n") 'emms-next)
137    (define-key map (kbd "p") 'emms-previous)
138    (define-key map (kbd "SPC") 'scroll-up)
139    (define-key map (kbd ">") 'emms-seek-forward)
140    (define-key map (kbd "<") 'emms-seek-backward)
141    (define-key map (kbd "P") 'emms-pause)
142    (define-key map (kbd "s") 'emms-stop)
143    (define-key map (kbd "f") 'emms-show)
144    (define-key map (kbd "c") 'emms-playlist-mode-center-current)
145    (define-key map (kbd "q") 'emms-playlist-mode-bury-buffer)
146    (define-key map (kbd "k") 'emms-playlist-mode-current-kill)
147    (define-key map (kbd "?") 'describe-mode)
148    (define-key map (kbd "r") 'emms-random)
149    (define-key map (kbd "C") 'emms-playlist-mode-clear)
150    (define-key map (kbd "d") 'emms-playlist-mode-goto-dired-at-point)
151    (define-key map (kbd "<mouse-2>") 'emms-playlist-mode-play-current-track)
152    (define-key map (kbd "RET") 'emms-playlist-mode-play-smart)
153    map)
154  "Keymap for `emms-playlist-mode'.")
155
156(defmacro emms-playlist-mode-move-wrapper (name fun)
157  "Create a function NAME which is an `interactive' version of FUN.
158
159NAME should be a symbol.
160FUN should be a function."
161  `(defun ,name ()
162     ,(format "Interactive wrapper around `%s' for playlist-mode."
163	      fun)
164     (interactive)
165     (,fun)))
166
167(emms-playlist-mode-move-wrapper emms-playlist-mode-first
168				 emms-playlist-first)
169
170(emms-playlist-mode-move-wrapper emms-playlist-mode-select-next
171				 emms-playlist-next)
172
173(emms-playlist-mode-move-wrapper emms-playlist-mode-select-previous
174				 emms-playlist-previous)
175
176(defun emms-playlist-mode-bury-buffer ()
177  "Wrapper around `bury-buffer' for popup windows."
178  (interactive)
179  (if emms-playlist-mode-popup-enabled
180      (unwind-protect
181	  (delete-window)
182	(setq emms-playlist-mode-popup-enabled nil))
183    (bury-buffer)))
184
185(defun emms-playlist-mode-current-kill ()
186  "If the current buffer is an EMMS playlist buffer, kill it.
187Otherwise, kill the current EMMS playlist buffer."
188  (interactive)
189  (if (and emms-playlist-buffer-p
190           (not (eq (current-buffer) emms-playlist-buffer)))
191      (kill-buffer (current-buffer))
192    (emms-playlist-current-kill)))
193
194(defun emms-playlist-mode-clear ()
195  "If the current buffer is an EMMS playlist buffer, clear it.
196Otherwise, clear the current EMMS playlist buffer."
197  (interactive)
198  (if (and emms-playlist-buffer-p
199           (not (eq (current-buffer) emms-playlist-buffer)))
200      (let ((inhibit-read-only t))
201        (widen)
202        (delete-region (point-min) (point-max)))
203    (emms-playlist-clear)))
204
205(defun emms-playlist-mode-last ()
206  "Move to directly after the last track in the current buffer."
207  (interactive)
208  (emms-playlist-ensure-playlist-buffer)
209  (let ((last (condition-case nil
210                  (save-excursion
211                    (goto-char (point-max))
212                    (point))
213                (error
214                 nil))))
215    (if last
216        (goto-char last)
217      (error "No last track"))))
218
219(defun emms-playlist-mode-center-current ()
220  "Move point to the currently selected track."
221  (interactive)
222  (goto-char (if emms-playlist-mode-selected-overlay
223                 (overlay-start emms-playlist-mode-selected-overlay)
224               (point-min))))
225
226(defun emms-playlist-mode-play-current-track ()
227  "Start playing track at point."
228  (interactive)
229  (emms-playlist-set-playlist-buffer (current-buffer))
230  (unless (emms-playlist-track-at (point))
231    (emms-playlist-next))
232  (emms-playlist-select (point))
233  (when emms-player-playing-p
234    (emms-stop))
235  (emms-start))
236
237(defun emms-playlist-mode-play-smart ()
238  "Determine the best operation to take on the current track.
239
240If on a playlist, and `emms-playlist-mode-open-playlists' is
241non-nil, load the playlist at point into a new buffer.
242
243Otherwise play the track immediately."
244  (interactive)
245  (save-excursion
246    ;; move to the start of the line, in case the point is on the \n,
247    ;; which isn't propertized
248    (emms-move-beginning-of-line nil)
249    (if (not emms-playlist-mode-open-playlists)
250        (emms-playlist-mode-play-current-track)
251      (unless (emms-playlist-track-at)
252        (emms-playlist-next))
253      (let* ((track (emms-playlist-track-at))
254             (name (emms-track-get track 'name))
255             (type (emms-track-get track 'type)))
256        (if (or (eq type 'playlist)
257                (and (eq type 'file)
258                     (string-match "\\.\\(m3u\\|pls\\)\\'" name)))
259            (emms-playlist-mode-load-playlist)
260          (emms-playlist-mode-play-current-track))))))
261
262(defun emms-playlist-mode-switch-buffer ()
263  "Switch to the playlist buffer and then switch back if called again.
264
265This function switches to the current Emms playlist buffer and
266remembers the buffer switched from. When called again the
267function switches back to the remembered buffer."
268  (interactive)
269  (if (eq (current-buffer)
270	  emms-playlist-buffer)
271      (switch-to-buffer emms-playlist-mode-switched-buffer)
272    (setq emms-playlist-mode-switched-buffer (current-buffer))
273    (switch-to-buffer emms-playlist-buffer)))
274
275(defun emms-playlist-mode-insert-newline ()
276  "Insert a newline at point."
277  (interactive)
278  (emms-with-inhibit-read-only-t
279   (newline)))
280
281(defun emms-playlist-mode-undo ()
282  "Wrapper around `undo'."
283  (interactive)
284  (emms-with-inhibit-read-only-t
285   (undo)))
286
287(defun emms-playlist-mode-add-contents ()
288  "Add files in the playlist at point to the current playlist buffer.
289
290If we are in the current playlist, make a new playlist buffer and
291set it as current."
292  (interactive)
293  (save-excursion
294    (emms-move-beginning-of-line nil)
295    (unless (emms-playlist-track-at)
296      (emms-playlist-next))
297    (let* ((track (emms-playlist-track-at))
298           (name (emms-track-get track 'name))
299           (type (emms-track-get track 'type))
300           (playlist-p (or (eq type 'playlist)
301                           (and (eq type 'file)
302                                (save-match-data
303                                  (string-match "\\.\\(m3u\\|pls\\)\\'"
304                                                name))))))
305      (emms-playlist-select (point))
306      (unless (and (buffer-live-p emms-playlist-buffer)
307                   (not (eq (current-buffer) emms-playlist-buffer)))
308        (setq emms-playlist-buffer
309              (emms-playlist-set-playlist-buffer (emms-playlist-new))))
310      (with-current-emms-playlist
311        (goto-char (point-max))
312        (when playlist-p
313          (insert (emms-track-force-description track) "\n"))
314        (let ((beg (point)))
315          (if playlist-p
316              (emms-add-playlist name)
317            (let ((func (intern (concat "emms-add-" (symbol-name type)))))
318              (if (functionp func)
319                  (funcall func name)
320                ;; fallback
321                (emms-add-file name))))
322          (when playlist-p
323            (goto-char (point-max))
324            (while (progn
325                     (forward-line -1)
326                     (>= (point) beg))
327              (insert "  ")))
328          (goto-char (point-min))
329          (message "Added %s" (symbol-name type)))))))
330
331(defun emms-playlist-mode-goto-dired-at-point ()
332  "Visit the track at point in a `dired' buffer."
333  (interactive)
334  (let ((track (emms-playlist-track-at)))
335    (if track
336	(let ((name (emms-track-get track 'name))
337	      (type (emms-track-get track 'type)))
338	  (if (eq type 'file)
339	      (dired (file-name-directory name))
340	    (error "Can't visit this track type in Dired")))
341      (error "No track at point"))))
342
343;;; --------------------------------------------------------
344;;; Killing and yanking
345;;; --------------------------------------------------------
346
347(defun emms-playlist-mode-between-p (p a b)
348  "Return t if P is a point between points A and B."
349  (and (<= a p)
350       (<= p b)))
351
352;; D
353(defun emms-playlist-mode-kill-entire-track ()
354  "Kill track at point, including newline."
355  (interactive)
356  (let ((kill-whole-line t))
357    (emms-playlist-mode-kill-track)))
358
359;; C-k
360(defun emms-playlist-mode-kill-track ()
361  "Kill track at point."
362  (interactive)
363  (emms-with-inhibit-read-only-t
364   (let ((track (emms-playlist-track-at)))
365     (when track
366       (let ((track-region (emms-property-region (point)
367						 'emms-track)))
368	 (when (and emms-player-playing-p
369		    (emms-playlist-selected-track-at-p))
370	   (emms-stop)
371	   (delete-overlay emms-playlist-mode-selected-overlay)
372	   (setq emms-playlist-mode-selected-overlay nil))))
373     (let ((kill-whole-line emms-playlist-mode-kill-whole-line-p))
374       (goto-char (point-at-bol))
375       (kill-line)))))
376
377;; C-w
378(defun emms-playlist-mode-kill ()
379  "Kill from mark to point."
380  (interactive)
381  (emms-with-inhibit-read-only-t
382   ;; Are we killing the playing/selected track?
383   (when (and (markerp emms-playlist-selected-marker)
384              (emms-playlist-mode-between-p
385               (marker-position emms-playlist-selected-marker)
386               (region-beginning)
387               (region-end)))
388     (emms-stop)
389     (delete-overlay emms-playlist-mode-selected-overlay)
390     (setq emms-playlist-mode-selected-overlay nil))
391   (kill-region (region-beginning)
392                (region-end))))
393
394(defun emms-playlist-mode-correct-previous-yank ()
395  "Fix the previous yank if needed."
396  (when (and (< (point-at-bol) (point))
397	     (< (point) (point-at-eol)))
398    (newline)))
399
400;; C-y
401(defun emms-playlist-mode-yank ()
402  "Yank into the playlist buffer."
403  (interactive)
404  (emms-with-inhibit-read-only-t
405   (goto-char (point-at-bol))
406   (yank)
407   (emms-playlist-mode-correct-previous-yank)))
408
409;; M-y
410(defun emms-playlist-mode-yank-pop ()
411  "Cycle through the kill-ring."
412  (interactive)
413  (emms-with-inhibit-read-only-t
414   (yank-pop nil)
415   (emms-playlist-mode-correct-previous-yank)))
416
417;;; --------------------------------------------------------
418;;; Overlay
419;;; --------------------------------------------------------
420
421(defun emms-playlist-mode-overlay-selected ()
422  "Place an overlay over the currently selected track."
423  (when emms-playlist-selected-marker
424    (save-excursion
425      (goto-char emms-playlist-selected-marker)
426      (let ((reg (emms-property-region (point) 'emms-track)))
427        (if emms-playlist-mode-selected-overlay
428            (move-overlay emms-playlist-mode-selected-overlay
429                          (car reg)
430                          (cdr reg))
431          (setq emms-playlist-mode-selected-overlay
432                (make-overlay (car reg)
433                              (cdr reg)
434                              nil t nil))
435          (overlay-put emms-playlist-mode-selected-overlay
436                       'face 'emms-playlist-selected-face)
437          (overlay-put emms-playlist-mode-selected-overlay
438                       'evaporate t))))))
439
440;;; --------------------------------------------------------
441;;; Saving/Restoring
442;;; --------------------------------------------------------
443
444(defun emms-playlist-mode-open-buffer (filename)
445  "Opens a previously saved playlist buffer.
446
447It creates a buffer called \"filename\", and restores the contents
448of the saved playlist inside."
449  (interactive "fFile: ")
450  (let* ((s)
451	 (buffer (get-buffer-create filename))
452	 (name   (buffer-name buffer)))
453    (with-current-buffer buffer
454      (emms-insert-file-contents filename)
455      (setq s (read (buffer-string))))
456    (kill-buffer buffer)
457    (with-current-buffer (emms-playlist-new name)
458      (emms-with-inhibit-read-only-t
459       (insert s)
460       (goto-char (point-min))
461       (emms-walk-tracks
462         (emms-playlist-update-track)))
463      (emms-playlist-first)
464      (emms-playlist-select (point))
465      (switch-to-buffer (current-buffer)))))
466
467(defun emms-playlist-mode-load-playlist ()
468  "Load the playlist into a new EMMS buffer.
469This preserves the current EMMS buffer."
470  (interactive)
471  (let* ((track (emms-playlist-track-at))
472         (name (emms-track-get track 'name))
473         (type (emms-track-get track 'type)))
474    (emms-playlist-select (point))
475    (run-hooks 'emms-player-stopped-hook)
476    (switch-to-buffer
477     (emms-playlist-set-playlist-buffer (emms-playlist-new)))
478    (emms-add-playlist name)))
479
480;;; --------------------------------------------------------
481;;; Local functions
482;;; --------------------------------------------------------
483
484(defun emms-playlist-mode-insert-track (track &optional no-newline)
485  "Insert the description of TRACK at point.
486When NO-NEWLINE is non-nil, do not insert a newline after the track."
487  (emms-playlist-ensure-playlist-buffer)
488  (emms-with-inhibit-read-only-t
489   (insert (emms-propertize (emms-track-force-description track)
490                            'emms-track track
491                            'face 'emms-playlist-track-face))
492   (when (emms-playlist-selected-track-at-p)
493     (emms-playlist-mode-overlay-selected))
494   (unless no-newline
495     (insert "\n"))))
496
497(defun emms-playlist-mode-update-track-function ()
498  "Update the track display at point."
499  (emms-playlist-ensure-playlist-buffer)
500  (emms-with-inhibit-read-only-t
501   (let ((track-region (emms-property-region (point)
502					     'emms-track))
503	 (track (get-text-property (point)
504				   'emms-track))
505	 (selectedp (emms-playlist-selected-track-at-p)))
506     (save-excursion
507       (delete-region (car track-region)
508		      (cdr track-region))
509       (when selectedp
510	 (delete-overlay emms-playlist-mode-selected-overlay)
511	 (setq emms-playlist-mode-selected-overlay nil))
512       (emms-playlist-mode-insert-track track t))
513     (when selectedp
514       (emms-playlist-select (point))))))
515
516;;; --------------------------------------------------------
517;;; Entry
518;;; --------------------------------------------------------
519
520(defun emms-playlist-mode-go ()
521  "Switch to the current emms-playlist buffer and use emms-playlist-mode."
522  (interactive)
523  (if (or (null emms-playlist-buffer)
524	  (not (buffer-live-p emms-playlist-buffer)))
525      (error "No current Emms buffer")
526    (switch-to-buffer emms-playlist-buffer)
527    (when (and (not (eq major-mode 'emms-playlist-mode))
528	       emms-playlist-buffer-p)
529      (emms-playlist-mode))
530    (when emms-playlist-mode-center-when-go
531      (emms-playlist-mode-center-current))))
532
533(defun emms ()
534  "Switch to the current emms-playlist buffer, use
535emms-playlist-mode and query for a directory tree to add to the
536playlist."
537  (interactive)
538  (if (or (null emms-playlist-buffer)
539	  (not (buffer-live-p emms-playlist-buffer)))
540      (call-interactively 'emms-add-file))
541  (emms-playlist-mode-go))
542
543(defun emms-playlist-mode-go-popup (&optional window-width)
544  "Popup emms-playlist buffer as a side window.
545
546Default value for WINDOW-WIDTH is `emms-playlist-mode-window-width'.
547WINDOW-WIDTH should be a positive integer."
548  (interactive)
549  (setq emms-playlist-mode-window-width
550        (round (or window-width emms-playlist-mode-window-width)))
551  (split-window-horizontally (- emms-playlist-mode-window-width))
552  (other-window 1)
553  (emms-playlist-mode-go)
554  (setq emms-playlist-mode-popup-enabled t))
555
556(defun emms-playlist-mode-next (arg)
557  "Navigate between playlists."
558  (interactive "p")
559  (let ((playlists (emms-playlist-buffer-list))
560        bufs idx)
561    (if playlists
562        ;; if not in playlist mode, switch to emms-playlist-buffer
563        (if (not (member (current-buffer) playlists))
564            (switch-to-buffer (if (and emms-playlist-buffer
565                                       (buffer-live-p emms-playlist-buffer))
566                                  emms-playlist-buffer
567                                (car playlists)))
568          (setq bufs (member (current-buffer) playlists))
569          (setq idx
570                (+ (- (length playlists) (length bufs))
571                   (if (> arg 0) 1 -1)))
572          (switch-to-buffer (nth (mod idx (length playlists)) playlists)))
573      (message "No playlist found!"))))
574(defun emms-playlist-mode-previous (arg)
575  (interactive "p")
576  (emms-playlist-mode-next (- arg)))
577
578(defun emms-playlist-mode-startup ()
579  "Instigate emms-playlist-mode on the current buffer."
580  ;; when there is neither a current emms track or a playing one...
581  (when (not (or emms-playlist-selected-marker
582		 emms-player-playing-p))
583    ;; ...then stop the player.
584    (emms-stop)
585    ;; why select the first track?
586    (when emms-playlist-buffer-p
587      (emms-playlist-select-first)))
588  ;; when there is a selected track.
589  (when emms-playlist-selected-marker
590    (emms-playlist-mode-overlay-selected))
591  (emms-with-inhibit-read-only-t
592   (add-text-properties (point-min)
593                        (point-max)
594                        '(face emms-playlist-track-face)))
595  (setq buffer-read-only t)
596  (setq truncate-lines t)
597  (setq buffer-undo-list nil))
598
599;;;###autoload
600(defun emms-playlist-mode ()
601  "A major mode for Emms playlists.
602\\{emms-playlist-mode-map}"
603  (interactive)
604  (let ((val emms-playlist-buffer-p))
605    (kill-all-local-variables)
606    (setq emms-playlist-buffer-p val))
607
608  (use-local-map emms-playlist-mode-map)
609  (setq major-mode 'emms-playlist-mode
610	mode-name "EMMS")
611
612  (setq emms-playlist-insert-track-function
613	'emms-playlist-mode-insert-track)
614  (setq emms-playlist-update-track-function
615	'emms-playlist-mode-update-track-function)
616  (add-hook 'emms-playlist-selection-changed-hook
617	    'emms-playlist-mode-overlay-selected)
618
619  (emms-playlist-mode-startup)
620
621  (run-hooks 'emms-playlist-mode-hook))
622
623(provide 'emms-playlist-mode)
624
625;;; emms-playlist-mode.el ends here
626