]> code.delx.au - gnu-emacs-elpa/blob - ampc.el
* ampc.el (ampc-handle-status): Run ampc-status-changed-hook.
[gnu-emacs-elpa] / ampc.el
1 ;;; ampc.el --- Asynchronous Music Player Controller -*- lexical-binding: t -*-
2
3 ;; Copyright (C) 2011-2012 Free Software Foundation, Inc.
4
5 ;; Author: Christopher Schmidt <christopher@ch.ristopher.com>
6 ;; Maintainer: Christopher Schmidt <christopher@ch.ristopher.com>
7 ;; Version: 0.1.3
8 ;; Created: 2011-12-06
9 ;; Keywords: ampc, mpc, mpd
10 ;; Compatibility: GNU Emacs: 24.x
11
12 ;; This file is part of ampc.
13
14 ;; This program is free software; you can redistribute it and/or modify
15 ;; it under the terms of the GNU General Public License as published by
16 ;; the Free Software Foundation, either version 3 of the License, or
17 ;; (at your option) any later version.
18
19 ;; This program is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 ;; GNU General Public License for more details.
23
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
26
27 ;;; Commentary:
28 ;;; * description
29 ;; ampc is a controller for the Music Player Daemon (http://mpd.wikia.com/).
30
31 ;;; ** installation
32 ;; If you use GNU ELPA, install ampc via M-x package-list-packages RET or
33 ;; (package-install 'ampc). Otherwise, grab the files in this repository and
34 ;; put the emacs lisp ones somewhere in your load-path or add the directory the
35 ;; files are in to it, e.g.:
36 ;;
37 ;; (add-to-list 'load-path "~/.emacs.d/ampc")
38 ;; (autoload 'ampc "ampc" nil t)
39 ;;
40 ;; Byte-compile ampc (M-x byte-compile-file RET /path/to/ampc.el RET) to improve
41 ;; its performance!
42
43 ;;; *** tagger
44 ;; ampc is not only a frontend to MPD but also a full-blown audio file tagger.
45 ;; To use this feature you have to build the backend application, `ampc_tagger',
46 ;; which in turn uses TagLib (http://taglib.github.com/), a dual-licended
47 ;; (LGPL/MPL) audio meta-data library written in C++. TagLib has no
48 ;; dependencies on its own.
49 ;;
50 ;; To build `ampc_tagger', locate ampc_tagger.cpp. The file can be found in the
51 ;; directory in which this file, ampc.el, is located. Compile the file and
52 ;; either customize `ampc-tagger-executable' to point to the binary file or move
53 ;; the executable in a suitable directory so Emacs finds it via consulting
54 ;; `exec-path'.
55 ;;
56 ;; g++ -O2 ampc_tagger.cpp -oampc_tagger -ltag && sudo cp ampc_tagger /usr/local/bin && rm ampc_tagger
57 ;;
58 ;; You have to customize `ampc-tagger-music-directories' in order to use the
59 ;; tagger. This variable should be a list of directories in which your music
60 ;; files are located. Usually this list should have only one entry, the value
61 ;; of your mpd.conf's `music_directory'.
62 ;;
63 ;; If `ampc-tagger-backup-directory' is non-nil, the tagger saved copies of all
64 ;; files that are about to be modified to this directory. Emacs's regular
65 ;; numeric backup filename syntax is used for the backup file names. By default
66 ;; `ampc-tagger-backup-directory' is set to "~/.emacs.d/ampc-backups/".
67
68 ;;; ** usage
69 ;; To invoke ampc call the command `ampc', e.g. via M-x ampc RET. The first
70 ;; argument to `ampc' is the host, the second is the port. Both values default
71 ;; to nil. If nil, ampc will use the value specified in `ampc-default-server',
72 ;; by default localhost:6600. To make ampc use the full frame rather than the
73 ;; selected window for its window setup, customise `ampc-use-full-frame' to a
74 ;; non-nil value.
75 ;;
76 ;; ampc offers three independent views which expose different parts of the user
77 ;; interface. The current playlist view, the default view at startup, may be
78 ;; accessed using the `J' key (that is `S-j'). The playlist view may be
79 ;; accessed using the `K' key. The outputs view may be accessed by pressing
80 ;; `L'.
81
82 ;;; *** current playlist view
83 ;; The playlist view looks like this:
84 ;;
85 ;; .........................
86 ;; . 1 . 3 . 4 . 5 .
87 ;; .......... . . .
88 ;; . 2 . . . .
89 ;; . . . . .
90 ;; . . . . .
91 ;; . ................
92 ;; . . 6 .
93 ;; . . .
94 ;; .........................
95 ;;
96 ;; Window one exposes basic information about the daemon, such as the current
97 ;; state (stop/play/pause), the song currently playing or the volume.
98 ;;
99 ;; All windows, except the status window, contain a tabular list of items. Each
100 ;; item may be selected/marked. There may be multiple selections.
101 ;;
102 ;; To mark an entry, move the point to the entry and press `m' (ampc-mark). To
103 ;; unmark an entry, press `u' (ampc-unmark). To unmark all entries, press `U'
104 ;; (ampc-unmark-all). To toggle marks, press `t' (ampc-toggle-marks). Pressing
105 ;; `<down-mouse-1>' with the mouse mouse cursor on a list entry will move point
106 ;; to the entry and toggle the mark. To navigate to the next entry, press `n'
107 ;; (ampc-next-line). Analogous, pressing `p' (ampc-previous-line) moves the
108 ;; point to the previous entry.
109 ;;
110 ;; Window two shows the current playlist. The song that is currently played by
111 ;; the daemon, if any, is highlighted. To delete the selected songs from the
112 ;; playlist, press `d' (ampc-delete). Pressing `<down-mouse-3>' will move the
113 ;; point to the entry under cursor and delete it from the playlist. To move the
114 ;; selected songs up, press `<up>' (ampc-up). Analogous, press `<down>'
115 ;; (ampc-down) to move the selected songs down. Pressing `<return>'
116 ;; (ampc-play-this) or `<down-mouse-2>' will play the song at point/cursor.
117 ;;
118 ;; Windows three to five are tag browsers. You use them to narrow the song
119 ;; database to certain songs. Think of tag browsers as filters, analogous to
120 ;; piping `grep' outputs through additional `grep' filters. The property of the
121 ;; songs that is filtered is displayed in the header line of the window.
122 ;;
123 ;; Window six shows the songs that match the filters defined by windows three to
124 ;; five. To add the selected song to the playlist, press `a' (ampc-add).
125 ;; Pressing `<down-mouse-3>' will move the point to the entry under the cursor
126 ;; and execute `ampc-add'. These key bindings works in tag browsers as well.
127 ;; Calling `ampc-add' in a tag browser adds all songs filtered up to the
128 ;; selected browser to the playlist.
129 ;;
130 ;; The tag browsers of the current playlist view (accessed via `J') are `Genre'
131 ;; (window 3), `Artist' (window 4) and `Album' (window 5). The key `M' may be
132 ;; used to fire up a slightly modified current playlist view. There is no
133 ;; difference to the default current playlist view other than that the tag
134 ;; browsers filter to `Genre' (window 3), `Album' (window 4) and `Artist'
135 ;; (window 5). Metaphorically speaking, the order of the `grep' filters defined
136 ;; by the tag browsers is different.
137
138 ;;; *** playlist view
139 ;; The playlist view resembles the current playlist view. The window, which
140 ;; exposes the playlist content, is replaced by three windows, vertically
141 ;; arragned, though. The top one still shows the current playlist. The bottom
142 ;; one shows a list of stored playlists. The middle window exposes the content
143 ;; of the selected (stored) playlist. All commands that used to work in the
144 ;; current playlist view and modify the current playlist now modify the selected
145 ;; (stored) playlist unless the point is within the current playlist buffer.
146 ;; The list of stored playlists is the only view in ampc that may have only one
147 ;; marked entry.
148 ;;
149 ;; To queue a playlist, press `l' (ampc-load) or `<down-mouse-2>'. To delete a
150 ;; playlist, press `d' (ampc-delete-playlist) or `<down-mouse-3>'. The command
151 ;; `ampc-rename-playlist', bound to `r', can be used to rename a playlist.
152 ;;
153 ;; Again, the key `<' may be used to setup a playlist view with a different
154 ;; order of tag browsers.
155
156 ;;; *** outputs view
157 ;; The outputs view contains a single list which shows the configured outputs of
158 ;; MPD. To toggle the enabled property of the selected outputs, press `a'
159 ;; (ampc-toggle-output-enabled) or `<mouse-3>'.
160
161 ;;; ** tagger
162 ;; To start the tagging subsystem, press `I' (ampc-tagger). This key binding
163 ;; works in every buffer associated with ampc. First, the command tries to
164 ;; determine which files you want to tag. The files are collected using either
165 ;; the selected entries within the current buffer, the file associated with the
166 ;; entry at point, or, if both sources did not provide any files, the audio file
167 ;; that is currently played by MPD. Next, the tagger view is created. On the
168 ;; right there is the buffer that contain the tag data. Each line in this
169 ;; buffer represents a tag with a value. Tag and value are separated by a
170 ;; colon. Valid tags are "Title", "Artist", "Album", "Comment", "Genre", "Year"
171 ;; and "Track". The value can be an arbitrary string. Whitespaces in front and
172 ;; at the end of the value are ignored. If the value is "<keep>", the tag line
173 ;; is ignored.
174 ;;
175 ;; To save the specified tag values back to the files, press `C-c C-c'
176 ;; (ampc-tagger-save). To exit the tagger and restore the previous window
177 ;; configuration, press `C-c C-q'. `C-u C-c C-c' saved the tags and exits the
178 ;; tagger. Only tags that are actually specified within the tagger buffer
179 ;; written back to the file. Other tags will not be touched by ampc. For
180 ;; example, to clear the "Commentary" tag, you need to specify the line
181 ;;
182 ;; Commentary:
183 ;;
184 ;; In the tagger buffer. Omitting this line will make the tagger not touch the
185 ;; "Commentary" tag at all.
186 ;;
187 ;; On the right there is the files list buffer. The selection of this buffer
188 ;; specifies which files the command `ampc-tag-save' will write to. If no file
189 ;; is selected, the file at point in the file list buffer is used.
190 ;;
191 ;; To reset the values of the tags specified in the tagger buffer to the common
192 ;; values of all selected files specified by the selection of the files list
193 ;; buffer, press `C-c C-r' (ampc-tagger-reset). With a prefix argument,
194 ;; `ampc-tagger-reset' restores missing tags as well.
195 ;;
196 ;; You can use tab-completion within the tagger buffer for both tags and tag
197 ;; values.
198 ;;
199 ;; You can also use the tagging subsystem on its own without a running ampc
200 ;; instance. To start the tagger, call `ampc-tag-files'. This function accepts
201 ;; one argument, a list of absolute file names which are the files to tag. ampc
202 ;; provides a minor mode for dired, `ampc-tagger-dired-mode'. If this mode is
203 ;; enabled within a dired buffer, pressing `C-c C-t' (ampc-tagger-dired) will
204 ;; start the tagger on the current selection.
205 ;;
206 ;; The following ampc-specific hooks are run during tagger usage:
207 ;;
208 ;; `ampc-tagger-grab-hook': Run by the tagger before grabbing tags of a file.
209 ;; Each function is called with one argument, the file name.
210 ;;
211 ;; `ampc-tagger-grabbed-hook': Run by the tagger after grabbing tags of a file.
212 ;; Each function is called with one argument, the file name.
213 ;;
214 ;; `ampc-tagger-store-hook': Run by the tagger before writing tags back to a
215 ;; file. Each function is called with two arguments, FOUND-CHANGED and DATA.
216 ;; FOUND-CHANGED is non-nil if the tags that are about to be written differ from
217 ;; the ones in the file. DATA is a cons. The car specifies the full file name
218 ;; of the file that is about to be written to, the cdr is an alist that
219 ;; specifies the tags that are about to be (over-)written. The car of each
220 ;; entry in this list is a symbol specifying the tag (one of the ones in
221 ;; `ampc-tagger-tags'), the cdr a string specifying the value. The cdr of DATA
222 ;; may be modified. If FOUND-CHANGED is nil and the cdr of DATA is not modified
223 ;; throughout the hook is run, the file is not touched.
224 ;; `ampc-tagger-stored-hook' is still run, though.
225 ;;
226 ;; `ampc-tagger-stored-hook': Run by the tagger after writing tags back to a
227 ;; file. Each function is called with two arguments, FOUND-CHANGED and DATA.
228 ;; These are the same arguments that were already passed to
229 ;; `ampc-tagger-store-hook'. The car of DATA, the file name, may be modified.
230 ;;
231 ;; These hooks can be used to handle vc locking and unlocking of files. For
232 ;; renaming files according to their (new) tag values, ampc provides the
233 ;; function `ampc-tagger-rename-artist-title' which may be added to
234 ;; `ampc-tagger-stored-hook'. The new file name generated by this function is
235 ;; "Artist"_-_"Title"."extension". Characters within "Artist" and "Title" that
236 ;; are not alphanumeric are substituted with underscores.
237
238 ;;; ** global keys
239 ;; Aside from `J', `M', `K', `<' and `L', which may be used to select different
240 ;; views, and `I' which starts the tagger, ampc defines the following global
241 ;; keys. These binding are available in every buffer associated with ampc:
242 ;;
243 ;; `k' (ampc-toggle-play): Toggle play state. If MPD does not play a song,
244 ;; start playing the song at point if the current buffer is the playlist buffer,
245 ;; otherwise start at the beginning of the playlist. With numeric prefix
246 ;; argument 4, stop player rather than pause if applicable.
247 ;;
248 ;; `l' (ampc-next): Play next song.
249 ;; `j' (ampc-previous): Play previous song
250 ;;
251 ;; `c' (ampc-clear): Clear playlist.
252 ;; `s' (ampc-shuffle): Shuffle playlist.
253 ;;
254 ;; `S' (ampc-store): Store playlist.
255 ;; `O' (ampc-load): Load selected playlist into the current playlist.
256 ;; `R' (ampc-rename-playlist): Rename selected playlist.
257 ;; `D' (ampc-delete-playlist): Delete selected playlist.
258 ;;
259 ;; `y' (ampc-increase-volume): Increase volume.
260 ;; `M-y' (ampc-decrease-volume): Decrease volume.
261 ;; `C-M-y' (ampc-set-volume): Set volume.
262 ;; `h' (ampc-increase-crossfade): Increase crossfade.
263 ;; `M-h' (ampc-decrease-crossfade): Decrease crossfade.
264 ;; `C-M-h' (ampc-set-crossfade): Set crossfade.
265 ;;
266 ;; `e' (ampc-toggle-repeat): Toggle repeat state.
267 ;; `r' (ampc-toggle-random): Toggle random state.
268 ;; `f' (ampc-toggle-consume): Toggle consume state.
269 ;;
270 ;; `P' (ampc-goto-current-song): Select the current playlist window and move
271 ;; point to the current song.
272 ;; `G' (ampc-mini): Select song to play via `completing-read'.
273 ;;
274 ;; `T' (ampc-trigger-update): Trigger a database update.
275 ;; `Z' (ampc-suspend): Suspend ampc.
276 ;; `q' (ampc-quit): Quit ampc.
277 ;;
278 ;; The keymap of ampc is designed to fit the QWERTY United States keyboard
279 ;; layout. If you use another keyboard layout, feel free to modify
280 ;; `ampc-mode-map'. For example, I use a regular QWERTZ German keyboard
281 ;; (layout), so I modify `ampc-mode-map' in my init.el like this:
282 ;;
283 ;; (eval-after-load 'ampc
284 ;; '(flet ((substitute-ampc-key
285 ;; (from to)
286 ;; (define-key ampc-mode-map to (lookup-key ampc-mode-map from))
287 ;; (define-key ampc-mode-map from nil)))
288 ;; (substitute-ampc-key (kbd "z") (kbd "Z"))
289 ;; (substitute-ampc-key (kbd "y") (kbd "z"))
290 ;; (substitute-ampc-key (kbd "M-y") (kbd "M-z"))
291 ;; (substitute-ampc-key (kbd "C-M-y") (kbd "C-M-z"))
292 ;; (substitute-ampc-key (kbd "<") (kbd ";"))))
293 ;;
294 ;; If ampc is suspended, you can still use every interactive command that does
295 ;; not directly operate on or with the user interace of ampc. For example it is
296 ;; perfectly fine to call `ampc-increase-volume' or `ampc-toggle-play' via M-x
297 ;; RET. Especially the commands `ampc-status' and `ampc-mini' are predesignated
298 ;; to be bound in the global keymap and called when ampc is suspended.
299 ;; `ampc-status' messages the information that is displayed by the status window
300 ;; of ampc. `ampc-mini' lets you select a song to play via `completing-read'.
301 ;; To start ampc suspended, call `ampc' with the third argument being non-nil.
302 ;; To check whether ampc is connected to the daemon and/or suspended, call
303 ;; `ampc-is-on-p' or `ampc-suspended-p'.
304 ;;
305 ;; (global-set-key (kbd "<f7>")
306 ;; (lambda ()
307 ;; (interactive)
308 ;; (unless (ampc-on-p)
309 ;; (ampc nil nil t))
310 ;; (ampc-status)))
311 ;; (global-set-key (kbd "<f8>")
312 ;; (lambda ()
313 ;; (interactive)
314 ;; (unless (ampc-on-p)
315 ;; (ampc nil nil t))
316 ;; (ampc-mini)))
317
318 ;;; Code:
319 ;;; * code
320 (eval-when-compile
321 (require 'cl))
322 (require 'network-stream)
323 (require 'avl-tree)
324
325 ;;; ** declarations
326 (defgroup ampc ()
327 "Asynchronous client for the Music Player Daemon."
328 :prefix "ampc-"
329 :group 'multimedia
330 :group 'applications)
331
332 ;;; *** customs
333 (defcustom ampc-debug nil
334 "Non-nil means log outgoing communication between ampc and MPD.
335 If the value is neither t nor nil, also log incoming data."
336 :type '(choice (const :tag "Disable" nil)
337 (const :tag "Outgoing" t)
338 (const :tag "Incoming and outgoing" full)))
339
340 (defcustom ampc-use-full-frame nil
341 "If non-nil, ampc will use the entire Emacs screen."
342 :type 'boolean)
343
344 (defcustom ampc-truncate-lines t
345 "If non-nil, truncate lines in ampc buffers."
346 :type 'boolean)
347
348 (defcustom ampc-default-server '("localhost" . 6600)
349 "The MPD server to connect to if the arguments to `ampc' are nil.
350 This variable is a cons cell, with the car specifying the
351 hostname and the cdr specifying the port. Both values can be
352 nil, which will make ampc query the user for values on each
353 invocation."
354 :type '(cons (choice :tag "Hostname"
355 (string)
356 (const :tag "Ask" nil))
357 (choice :tag "Port"
358 (string)
359 (integer)
360 (const :tag "Ask" nil))))
361
362 (defcustom ampc-synchronous-commands '(t status currentsong play)
363 "List of MPD commands that should be executed synchronously.
364 Executing commands that print lots of output synchronously will
365 result in massive performance improvements of ampc. If the car
366 of this list is `t', execute all commands synchronously other
367 than the ones specified by the rest of the list."
368 :type '(repeat symbol))
369
370 (defcustom ampc-status-tags nil
371 "List of additional tags of the current song that are added to
372 the internal status of ampc and thus are passed to the functions
373 in `ampc-status-changed-hook'. Each element may be a string that
374 specifies a tag that is returned by MPD's `currentsong'
375 command."
376 :type '(list symbol))
377
378 (defcustom ampc-volume-step 5
379 "Default step of `ampc-increase-volume' and
380 `ampc-decrease-volume' for changing the volume."
381 :type 'integer)
382
383 (defcustom ampc-crossfade-step 5
384 "Default step of `ampc-increase-crossfade' and
385 `ampc-decrease-crossfade' for changing the crossfade."
386 :type 'integer)
387
388 (defcustom ampc-tag-transform-funcs '(("Time" . ampc-transform-time)
389 ("Track" . ampc-transform-track))
390 "Alist of tag treatment functions.
391 The car, a string, of each entry specifies the MPD tag, the cdr a
392 function which transforms the tag to the value that should be
393 used by ampc. The function is called with one string argument,
394 the tag value, and should return the treated value."
395 :type '(alist :key-type string :value-type function))
396
397 (defcustom ampc-tagger-music-directories nil
398 "List of base directories in which your music files are located.
399 Usually this list should have only one entry, the value of your
400 mpd.conf's `music_directory'"
401 :type '(list directory))
402
403 (defcustom ampc-tagger-executable "ampc_tagger"
404 "The name or full path to ampc's tagger executable."
405 :type 'string)
406
407 (defcustom ampc-tagger-backup-directory
408 (file-name-directory (locate-user-emacs-file "ampc-backups/"))
409 "The directory in which the tagger copies files before modifying.
410 If nil, disable backups."
411 :type '(choice (const :tag "Disable backups" nil)
412 (directory :tag "Directory")))
413
414 ;;; **** hooks
415 (defcustom ampc-before-startup-hook nil
416 "A hook run before startup.
417 This hook is called as the first thing when ampc is started."
418 :type 'hook)
419
420 (defcustom ampc-connected-hook nil
421 "A hook run after ampc connected to MPD."
422 :type 'hook)
423
424 (defcustom ampc-suspend-hook nil
425 "A hook run when suspending ampc."
426 :type 'hook)
427
428 (defcustom ampc-quit-hook nil
429 "A hook run when exiting ampc."
430 :type 'hook)
431
432 (defcustom ampc-status-changed-hook nil
433 "A hook run whenever the status of the daemon (that is volatile
434 properties such as volume or current song) changes. The hook is
435 run with one arg, an alist that contains the new status. The car
436 of each entry is a symbol, the cdr is a string. Valid keys are:
437
438 volume
439 repeat
440 random
441 consume
442 xfade
443 state
444 song
445 Artist
446 Title
447
448 and the keys in `ampc-status-tags'. Not all keys may be present
449 all the time!"
450 :type 'hook)
451
452 (defcustom ampc-tagger-grab-hook nil
453 "Hook run by the tagger before grabbing tags of a file.
454 Each function is called with one argument, the file name."
455 :type 'hook)
456 (defcustom ampc-tagger-grabbed-hook nil
457 "Hook run by the tagger after grabbing tags of a file.
458 Each function is called with one argument, the file name."
459 :type 'hook)
460
461 (defcustom ampc-tagger-store-hook nil
462 "Hook run by the tagger before writing tags back to a file.
463 Each function is called with two arguments, FOUND-CHANGED and
464 DATA. FOUND-CHANGED is non-nil if the tags that are about to be
465 written differ from the ones in the file. DATA is a cons. The
466 car specifies the full file name of the file that is about to be
467 written to, the cdr is an alist that specifies the tags that are
468 about to be (over-)written. The car of each entry in this list
469 is a symbol specifying the tag (one of the ones in
470 `ampc-tagger-tags'), the cdr a string specifying the value. The
471 cdr of DATA may be modified. If FOUND-CHANGED is nil and the cdr
472 of DATA is not modified throughout the hook is run, the file is
473 not touched. `ampc-tagger-stored-hook' is still run, though."
474 :type 'hook)
475 (defcustom ampc-tagger-stored-hook nil
476 "Hook run by the tagger after writing tags back to a file.
477 Each function is called with two arguments, FOUND-CHANGED and
478 DATA. These are the same arguments that were already passed to
479 `ampc-tagger-store-hook'. The car of DATA, the file name, may be
480 modified."
481 :type 'hook)
482
483 ;;; *** faces
484 (defface ampc-mark-face '((t (:inherit font-lock-constant-face)))
485 "Face of the mark.")
486 (defface ampc-marked-face '((t (:inherit warning)))
487 "Face of marked entries.")
488 (defface ampc-unmarked-face '((t (:inerhit default)))
489 "Face of unmarked entries.")
490 (defface ampc-current-song-mark-face '((t (:inherit region)))
491 "Face of mark of the current song.")
492 (defface ampc-current-song-marked-face '((t (:inherit region)))
493 "Face of the current song if marked.")
494
495 (defface ampc-tagger-tag-face '((t (:inherit font-lock-constant-face)))
496 "Face of tags within the tagger.")
497 (defface ampc-tagger-keyword-face '((t (:inherit font-lock-keyword-face)))
498 "Face of tags within the tagger.")
499
500 ;;; *** internal variables
501 (defvar ampc-views
502 (let* ((songs '(1.0 song :properties (("Track" :title "#" :width 4)
503 ("Title" :min 15 :max 40)
504 ("Time" :width 6)
505 ("Artist" :min 15 :max 40)
506 ("Album" :min 15 :max 40))))
507 (rs_a `(1.0 vertical
508 (0.7 horizontal
509 (0.33 tag :tag "Genre" :id 1 :select t)
510 (0.33 tag :tag "Artist" :id 2)
511 (1.0 tag :tag "Album" :id 3))
512 ,songs))
513 (rs_b `(1.0 vertical
514 (0.7 horizontal
515 (0.33 tag :tag "Genre" :id 1 :select t)
516 (0.33 tag :tag "Album" :id 2)
517 (1.0 tag :tag "Artist" :id 3))
518 ,songs))
519 (pl-prop '(:properties (("Title" :min 15 :max 40)
520 ("Artist" :min 15 :max 40)
521 ("Album" :min 15 :max 40)
522 ("Time" :width 6)))))
523 `((tagger
524 horizontal
525 (0.65 files-list
526 :properties ((filename :shrink t :title "File" :min 20 :max 40)
527 ("Title" :min 15 :max 40)
528 ("Artist" :min 15 :max 40)
529 ("Album" :min 15 :max 40)
530 ("Genre" :min 15 :max 40)
531 ("Year" :width 5)
532 ("Track" :title "#" :width 4)
533 ("Comment" :min 15 :max 40))
534 :dedicated nil)
535 (1.0 tagger))
536 ("Current playlist view (Genre|Artist|Album)"
537 ,(kbd "J")
538 horizontal
539 (0.4 vertical
540 (6 status)
541 (1.0 current-playlist ,@pl-prop))
542 ,rs_a)
543 ("Current playlist view (Genre|Album|Artist)"
544 ,(kbd "M")
545 horizontal
546 (0.4 vertical
547 (6 status)
548 (1.0 current-playlist ,@pl-prop))
549 ,rs_b)
550 ("Playlist view (Genre|Artist|Album)"
551 ,(kbd "K")
552 horizontal
553 (0.4 vertical
554 (6 status)
555 (1.0 vertical
556 (0.4 current-playlist ,@pl-prop)
557 (0.4 playlist ,@pl-prop)
558 (1.0 playlists)))
559 ,rs_a)
560 ("Playlist view (Genre|Album|Artist)"
561 ,(kbd "<")
562 horizontal
563 (0.4 vertical
564 (6 status)
565 (1.0 vertical
566 (0.4 current-playlist ,@pl-prop)
567 (0.4 playlist ,@pl-prop)
568 (1.0 playlists)))
569 ,rs_b)
570 ("Outputs view"
571 ,(kbd "L")
572 outputs :properties (("outputname" :title "Name" :min 10 :max 30)
573 ("outputenabled" :title "Enabled" :width 9))))))
574
575 (defvar ampc-connection nil)
576 (defvar ampc-host nil)
577 (defvar ampc-port nil)
578 (defvar ampc-outstanding-commands nil)
579
580 (defvar ampc-no-implicit-next-dispatch nil)
581 (defvar ampc-working-timer nil)
582 (defvar ampc-yield nil)
583 (defvar ampc-yield-redisplay nil)
584
585 (defvar ampc-windows nil)
586 (defvar ampc-all-buffers nil)
587
588 (defvar ampc-type nil)
589 (make-variable-buffer-local 'ampc-type)
590 (defvar ampc-dirty nil)
591 (make-variable-buffer-local 'ampc-dirty)
592
593 (defvar ampc-internal-db nil)
594 (defvar ampc-status nil)
595
596 (defvar ampc-tagger-previous-configuration nil)
597 (defvar ampc-tagger-version-verified nil)
598 (defvar ampc-tagger-completion-all-files nil)
599 (defvar ampc-tagger-genres nil)
600
601 (defconst ampc-tagger-version "0.1")
602 (defconst ampc-tagger-tags '(Title Artist Album Comment Genre Year Track))
603
604 ;;; *** mode maps
605 (defvar ampc-mode-map
606 (let ((map (make-sparse-keymap)))
607 (suppress-keymap map)
608 (define-key map (kbd "k") 'ampc-toggle-play)
609 (define-key map (kbd "l") 'ampc-next)
610 (define-key map (kbd "j") 'ampc-previous)
611 (define-key map (kbd "c") 'ampc-clear)
612 (define-key map (kbd "s") 'ampc-shuffle)
613 (define-key map (kbd "S") 'ampc-store)
614 (define-key map (kbd "O") 'ampc-load)
615 (define-key map (kbd "R") 'ampc-rename-playlist)
616 (define-key map (kbd "D") 'ampc-delete-playlist)
617 (define-key map (kbd "y") 'ampc-increase-volume)
618 (define-key map (kbd "M-y") 'ampc-decrease-volume)
619 (define-key map (kbd "C-M-y") 'ampc-set-volume)
620 (define-key map (kbd "h") 'ampc-increase-crossfade)
621 (define-key map (kbd "M-h") 'ampc-decrease-crossfade)
622 (define-key map (kbd "C-M-h") 'ampc-set-crossfade)
623 (define-key map (kbd "e") 'ampc-toggle-repeat)
624 (define-key map (kbd "r") 'ampc-toggle-random)
625 (define-key map (kbd "f") 'ampc-toggle-consume)
626 (define-key map (kbd "P") 'ampc-goto-current-song)
627 (define-key map (kbd "G") 'ampc-mini)
628 (define-key map (kbd "q") 'ampc-quit)
629 (define-key map (kbd "z") 'ampc-suspend)
630 (define-key map (kbd "T") 'ampc-trigger-update)
631 (define-key map (kbd "I") 'ampc-tagger)
632 (loop for view in ampc-views
633 do (when (stringp (car view))
634 (define-key map (cadr view)
635 `(lambda ()
636 (interactive)
637 (ampc-change-view ',view)))))
638 map))
639
640 (defvar ampc-item-mode-map
641 (let ((map (make-sparse-keymap)))
642 (suppress-keymap map)
643 (define-key map (kbd "m") 'ampc-mark)
644 (define-key map (kbd "u") 'ampc-unmark)
645 (define-key map (kbd "U") 'ampc-unmark-all)
646 (define-key map (kbd "n") 'ampc-next-line)
647 (define-key map (kbd "p") 'ampc-previous-line)
648 (define-key map (kbd "<down-mouse-1>") 'ampc-mouse-toggle-mark)
649 (define-key map (kbd "<mouse-1>") 'ampc-mouse-align-point)
650 (define-key map [remap next-line] 'ampc-next-line)
651 (define-key map [remap previous-line] 'ampc-previous-line)
652 (define-key map [remap tab-to-tab-stop] 'ampc-move-to-tab)
653 map))
654
655 (defvar ampc-current-playlist-mode-map
656 (let ((map (make-sparse-keymap)))
657 (suppress-keymap map)
658 (define-key map (kbd "<return>") 'ampc-play-this)
659 (define-key map (kbd "<down-mouse-2>") 'ampc-mouse-play-this)
660 (define-key map (kbd "<mouse-2>") 'ampc-mouse-align-point)
661 (define-key map (kbd "<down-mouse-3>") 'ampc-mouse-delete)
662 (define-key map (kbd "<mouse-3>") 'ampc-mouse-align-point)
663 map))
664
665 (defvar ampc-playlist-mode-map
666 (let ((map (make-sparse-keymap)))
667 (suppress-keymap map)
668 (define-key map (kbd "t") 'ampc-toggle-marks)
669 (define-key map (kbd "d") 'ampc-delete)
670 (define-key map (kbd "<up>") 'ampc-up)
671 (define-key map (kbd "<down>") 'ampc-down)
672 (define-key map (kbd "<down-mouse-3>") 'ampc-mouse-delete)
673 (define-key map (kbd "<mouse-3>") 'ampc-mouse-align-point)
674 map))
675
676 (defvar ampc-playlists-mode-map
677 (let ((map (make-sparse-keymap)))
678 (suppress-keymap map)
679 (define-key map (kbd "l") 'ampc-load)
680 (define-key map (kbd "r") 'ampc-rename-playlist)
681 (define-key map (kbd "d") 'ampc-delete-playlist)
682 (define-key map (kbd "<down-mouse-2>") 'ampc-mouse-load)
683 (define-key map (kbd "<mouse-2>") 'ampc-mouse-align-point)
684 (define-key map (kbd "<down-mouse-3>") 'ampc-mouse-delete-playlist)
685 (define-key map (kbd "<mouse-3>") 'ampc-mouse-align-point)
686 map))
687
688 (defvar ampc-tag-song-mode-map
689 (let ((map (make-sparse-keymap)))
690 (suppress-keymap map)
691 (define-key map (kbd "t") 'ampc-toggle-marks)
692 (define-key map (kbd "a") 'ampc-add)
693 (define-key map (kbd "<down-mouse-3>") 'ampc-mouse-add)
694 (define-key map (kbd "<mouse-3>") 'ampc-mouse-align-point)
695 map))
696
697 (defvar ampc-outputs-mode-map
698 (let ((map (make-sparse-keymap)))
699 (suppress-keymap map)
700 (define-key map (kbd "t") 'ampc-toggle-marks)
701 (define-key map (kbd "a") 'ampc-toggle-output-enabled)
702 (define-key map (kbd "<down-mouse-3>") 'ampc-mouse-toggle-output-enabled)
703 (define-key map (kbd "<mouse-3>") 'ampc-mouse-align-point)
704 map))
705
706 (defvar ampc-files-list-mode-map
707 (let ((map (make-sparse-keymap)))
708 (suppress-keymap map)
709 (define-key map (kbd "t") 'ampc-toggle-marks)
710 (define-key map (kbd "C-c C-q") 'ampc-tagger-quit)
711 (define-key map (kbd "C-c C-c") 'ampc-tagger-save)
712 (define-key map (kbd "C-c C-r") 'ampc-tagger-reset)
713 (define-key map [remap ampc-tagger] nil)
714 (define-key map [remap ampc-quit] 'ampc-tagger-quit)
715 (loop for view in ampc-views
716 do (when (stringp (car view))
717 (define-key map (cadr view) nil)))
718 map))
719
720 (defvar ampc-tagger-mode-map
721 (let ((map (make-sparse-keymap)))
722 (define-key map (kbd "C-c C-q") 'ampc-tagger-quit)
723 (define-key map (kbd "C-c C-c") 'ampc-tagger-save)
724 (define-key map (kbd "C-c C-r") 'ampc-tagger-reset)
725 (define-key map (kbd "<tab>") 'ampc-tagger-completion-at-point)
726 map))
727
728 (defvar ampc-tagger-dired-mode-map
729 (let ((map (make-sparse-keymap)))
730 (define-key map (kbd "C-c C-t") 'ampc-tagger-dired)
731 map))
732
733 ;;; **** menu
734 (easy-menu-define nil ampc-mode-map nil
735 `("ampc"
736 ("Change view" ,@(loop for view in ampc-views
737 when (stringp (car view))
738 collect (vector (car view)
739 `(lambda ()
740 (interactive)
741 (ampc-change-view ',view)))
742 end))
743 ["Run tagger" ampc-tagger]
744 "--"
745 ["Play" ampc-toggle-play
746 :visible (and ampc-status
747 (not (equal (cdr (assq 'state ampc-status)) "play")))]
748 ["Pause" ampc-toggle-play
749 :visible (and ampc-status
750 (equal (cdr (assq 'state ampc-status)) "play"))]
751 ["Stop" (lambda () (interactive) (ampc-toggle-play 4))
752 :visible (and ampc-status
753 (equal (cdr (assq 'state ampc-status)) "play"))]
754 ["Next" ampc-next]
755 ["Previous" ampc-previous]
756 "--"
757 ["Clear playlist" ampc-clear]
758 ["Shuffle playlist" ampc-shuffle]
759 ["Store playlist" ampc-store]
760 ["Queue Playlist" ampc-load :visible (ampc-playlist)]
761 ["Rename Playlist" ampc-rename-playlist :visible (ampc-playlist)]
762 ["Delete Playlist" ampc-delete-playlist :visible (ampc-playlist)]
763 "--"
764 ["Increase volume" ampc-increase-volume]
765 ["Decrease volume" ampc-decrease-volume]
766 ["Set volume" ampc-set-volume]
767 ["Increase crossfade" ampc-increase-crossfade]
768 ["Decrease crossfade" ampc-decrease-crossfade]
769 ["Set crossfade" ampc-set-crossfade]
770 ["Toggle repeat" ampc-toggle-repeat
771 :style toggle
772 :selected (equal (cdr (assq 'repeat ampc-status)) "1")]
773 ["Toggle random" ampc-toggle-random
774 :style toggle
775 :selected (equal (cdr (assq 'random ampc-status)) "1")]
776 ["Toggle consume" ampc-toggle-consume
777 :style toggle
778 :selected (equal (cdr (assq 'consume ampc-status)) "1")]
779 "--"
780 ["Trigger update" ampc-trigger-update]
781 ["Suspend" ampc-suspend]
782 ["Quit" ampc-quit]))
783
784 (easy-menu-define ampc-selection-menu ampc-item-mode-map
785 "Selection menu for ampc"
786 '("ampc Mark"
787 ["Add to playlist" ampc-add
788 :visible (not (eq (car ampc-type) 'outputs))]
789 ["Toggle enabled" ampc-toggle-output-enabled
790 :visible (eq (car ampc-type) 'outputs)]
791 "--"
792 ["Next line" ampc-next-line]
793 ["Previous line" ampc-previous-line]
794 ["Mark" ampc-mark]
795 ["Unmark" ampc-unmark]
796 ["Unmark all" ampc-unmark-all]
797 ["Toggle marks" ampc-toggle-marks
798 :visible (not (eq (car ampc-type) 'playlists))]))
799
800 (defvar ampc-tool-bar-map
801 (let ((map (make-sparse-keymap)))
802 (tool-bar-local-item
803 "mpc/prev" 'ampc-previous 'previous map
804 :help "Previous")
805 (tool-bar-local-item
806 "mpc/play" 'ampc-toggle-play 'play map
807 :help "Play"
808 :visible '(and ampc-status
809 (not (equal (cdr (assq 'state ampc-status)) "play"))))
810 (tool-bar-local-item
811 "mpc/pause" 'ampc-toggle-play 'pause map
812 :help "Pause"
813 :visible '(and ampc-status
814 (equal (cdr (assq 'state ampc-status)) "play")))
815 (tool-bar-local-item
816 "mpc/stop" (lambda () (interactive) (ampc-toggle-play 4)) 'stop map
817 :help "Stop"
818 :visible '(and ampc-status
819 (equal (cdr (assq 'state ampc-status)) "play")))
820 (tool-bar-local-item
821 "mpc/next" 'ampc-next 'next map
822 :help "Next")
823 map))
824
825 ;;; ** code
826 ;;; *** macros
827 (defmacro ampc-with-buffer (type &rest body)
828 (declare (indent 1) (debug t))
829 `(let* ((type- ,type)
830 (w (if (windowp type-)
831 type-
832 (loop for w in (ampc-normalize-windows)
833 thereis (when (with-current-buffer
834 (window-buffer w)
835 (etypecase type-
836 (symbol (eq (car ampc-type) type-))
837 (cons (equal ampc-type type-))))
838 w)))))
839 (when w
840 (with-selected-window w
841 (with-current-buffer (window-buffer w)
842 (let ((inhibit-read-only t))
843 ,@(if (eq (car body) 'no-se)
844 (cdr body)
845 `((save-excursion
846 (goto-char (point-min))
847 ,@body)))))))))
848
849 (defmacro ampc-fill-skeleton (tag &rest body)
850 (declare (indent 1) (debug t))
851 `(let ((tag- ,tag)
852 (data-buffer (current-buffer)))
853 (ampc-with-buffer tag-
854 no-se
855 (let ((old-point-data (get-text-property (point) 'cmp-data))
856 (old-window-start-offset
857 (1- (count-lines (window-start) (point)))))
858 (put-text-property (point-min) (point-max) 'not-updated t)
859 (when (eq ampc-dirty 'erase)
860 (put-text-property (point-min) (point-max) 'data nil))
861 (goto-char (point-min))
862 ,@body
863 (goto-char (point-min))
864 (loop until (eobp)
865 do (if (get-text-property (point) 'not-updated)
866 (kill-line 1)
867 (add-text-properties (+ (point) 2)
868 (progn (forward-line nil)
869 (1- (point)))
870 '(mouse-face highlight))))
871 (remove-text-properties (point-min) (point-max) '(not-updated))
872 (goto-char (point-min))
873 (when old-point-data
874 (loop until (eobp)
875 do (when (equal (get-text-property (point) 'cmp-data)
876 old-point-data)
877 (set-window-start
878 nil
879 (save-excursion
880 (forward-line (- old-window-start-offset))
881 (point))
882 t)
883 (return))
884 (forward-line)
885 finally do (goto-char (point-min)))))
886 (let ((effective-height (- (window-height)
887 (if mode-line-format 1 0)
888 (if header-line-format 1 0))))
889 (when (< (- (1- (line-number-at-pos (point-max)))
890 (line-number-at-pos (window-start)))
891 effective-height)
892 (set-window-start nil
893 (save-excursion
894 (goto-char (point-max))
895 (forward-line (- (1+ effective-height)))
896 (point))
897 t)))
898 (ampc-align-point)
899 (ampc-set-dirty nil))))
900
901 (defmacro ampc-with-selection (arg &rest body)
902 (declare (indent 1) (debug t))
903 `(let ((arg- ,arg))
904 (if (or (and (not arg-)
905 (save-excursion
906 (goto-char (point-min))
907 (search-forward-regexp "^* " nil t)))
908 (and arg- (symbolp arg-)))
909 (loop initially do (goto-char (point-min))
910 finally do (ampc-align-point)
911 while (search-forward-regexp "^* " nil t)
912 for index from 0
913 do (save-excursion
914 ,@body))
915 (setf arg- (prefix-numeric-value arg-))
916 (ampc-align-point)
917 (loop until (eobp)
918 for index from 0 to (1- (abs arg-))
919 do (save-excursion
920 ,@body)
921 until (if (< arg- 0) (ampc-previous-line) (ampc-next-line))))))
922
923 (defmacro ampc-iterate-source (data-buffer delimiter bindings &rest body)
924 (declare (indent 3) (debug t))
925 (when (memq (intern delimiter) bindings)
926 (callf2 delq (intern delimiter) bindings)
927 (push (list (intern delimiter)
928 '(buffer-substring (point) (line-end-position)))
929 bindings))
930 `(,@(if data-buffer `(with-current-buffer ,data-buffer) '(progn))
931 (when (search-forward-regexp
932 ,(concat "^" (regexp-quote delimiter) ": ")
933 nil t)
934 (loop with next
935 do (save-restriction
936 (setf next (ampc-narrow-entry
937 ,(concat "^" (regexp-quote delimiter) ": ")))
938 (let ,(loop for binding in bindings
939 if (consp binding)
940 collect binding
941 else
942 collect `(,binding (ampc-extract
943 (ampc-extract-regexp
944 ,(symbol-name binding))))
945 end)
946 ,@body))
947 while next
948 do (goto-char next)))))
949
950 (defmacro ampc-iterate-source-output (delimiter bindings pad-data &rest body)
951 (declare (indent 2) (debug t))
952 `(let ((output-buffer (current-buffer))
953 (tags (loop for (tag . props) in
954 (plist-get (cdr ampc-type) :properties)
955 collect (cons tag (ampc-extract-regexp tag)))))
956 (ampc-iterate-source
957 data-buffer ,delimiter ,bindings
958 (let ((pad-data ,pad-data))
959 (with-current-buffer output-buffer
960 (ampc-insert (ampc-pad pad-data) ,@body))))))
961
962 (defmacro ampc-extract-regexp (tag)
963 (if (stringp tag)
964 (concat "^" (regexp-quote tag) ": \\(.*\\)$")
965 `(concat "^" (regexp-quote ,tag) ": \\(.*\\)$")))
966
967 (defmacro ampc-tagger-log (&rest what)
968 (declare (indent 0) (debug t))
969 `(with-current-buffer (get-buffer-create "*Tagger Log*")
970 (ampc-tagger-log-mode)
971 (save-excursion
972 (goto-char (point-max))
973 (let ((inhibit-read-only t)
974 (what (concat ,@what)))
975 (when ampc-debug
976 (message "ampc: %s" what))
977 (insert what)))))
978
979 ;;; *** modes
980 (define-derived-mode ampc-outputs-mode ampc-item-mode "ampc-o")
981
982 (define-derived-mode ampc-tag-song-mode ampc-item-mode "ampc-ts")
983
984 (define-derived-mode ampc-current-playlist-mode ampc-playlist-mode "ampc-cpl"
985 (ampc-highlight-current-song-mode))
986
987 (define-derived-mode ampc-playlist-mode ampc-item-mode "ampc-pl")
988
989 (define-derived-mode ampc-playlists-mode ampc-item-mode "ampc-pls")
990
991 (define-derived-mode ampc-files-list-mode ampc-item-mode "ampc-files-list")
992
993 (define-derived-mode ampc-tagger-mode nil "ampc-tagger"
994 (set (make-local-variable 'tool-bar-map) ampc-tool-bar-map)
995 (set (make-local-variable 'tab-stop-list)
996 (list (+ (loop for tag in ampc-tagger-tags
997 maximize (length (symbol-name tag)))
998 2)))
999 (set (make-local-variable 'completion-at-point-functions)
1000 '(ampc-tagger-complete-tag ampc-tagger-complete-value))
1001 (setf truncate-lines ampc-truncate-lines
1002 font-lock-defaults
1003 `(((,(concat "^\\([ \t]*\\(?:"
1004 (mapconcat 'symbol-name ampc-tagger-tags "\\|")
1005 "\\)[ \t]*:\\)"
1006 "\\(\\(?:[ \t]*"
1007 "\\(?:"
1008 (mapconcat 'identity ampc-tagger-genres "\\|") "\\|<keep>"
1009 "\\)"
1010 "[ \t]*$\\)?\\)")
1011 (1 'ampc-tagger-tag-face)
1012 (2 'ampc-tagger-keyword-face)))
1013 t)))
1014
1015 (define-derived-mode ampc-tagger-log-mode nil "ampc-tagger-log")
1016
1017 (define-derived-mode ampc-item-mode ampc-mode "ampc-item"
1018 (setf font-lock-defaults '((("^\\(\\*\\)\\(.*\\)$"
1019 (1 'ampc-mark-face)
1020 (2 'ampc-marked-face))
1021 ("" 0 'ampc-unmarked-face))
1022 t)))
1023
1024 (define-derived-mode ampc-mode special-mode "ampc"
1025 (buffer-disable-undo)
1026 (set (make-local-variable 'tool-bar-map) ampc-tool-bar-map)
1027 (setf truncate-lines ampc-truncate-lines
1028 mode-line-modified "--"))
1029
1030 (define-minor-mode ampc-highlight-current-song-mode ""
1031 nil
1032 nil
1033 nil
1034 (funcall (if ampc-highlight-current-song-mode
1035 'font-lock-add-keywords
1036 'font-lock-remove-keywords)
1037 nil
1038 '((ampc-find-current-song
1039 (1 'ampc-current-song-mark-face)
1040 (2 'ampc-current-song-marked-face)))))
1041
1042 ;;;###autoload
1043 (define-minor-mode ampc-tagger-dired-mode
1044 "Minor mode that adds a audio file meta data tagging key binding to dired."
1045 nil
1046 " ampc-tagger"
1047 nil
1048 (assert (derived-mode-p 'dired-mode)))
1049
1050 ;;; *** internal functions
1051 (defun ampc-tagger-report (args status)
1052 (unless (zerop status)
1053 (let ((message (format (concat "ampc_tagger (%s %s) returned with a "
1054 "non-zero exit status (%s)")
1055 ampc-tagger-executable
1056 (mapconcat 'identity args " ")
1057 status)))
1058 (ampc-tagger-log message "\n")
1059 (error message))))
1060
1061 (defun ampc-tagger-call (&rest args)
1062 (ampc-tagger-report
1063 args
1064 (apply 'call-process ampc-tagger-executable nil t nil args)))
1065
1066 (defun ampc-int-insert-cmp (p1 p2)
1067 (cond ((< p1 p2) 'insert)
1068 ((eq p1 p2) 'overwrite)
1069 (t (- p1 p2))))
1070
1071 (defun ampc-normalize-windows ()
1072 (setf ampc-windows
1073 (loop for (window . buffer) in ampc-windows
1074 collect (cons (if (and (window-live-p window)
1075 (eq (window-buffer window) buffer))
1076 window
1077 (get-buffer-window buffer))
1078 buffer)))
1079 (delq nil (mapcar 'car ampc-windows)))
1080
1081 (defun ampc-restore-window-configuration ()
1082 (let ((windows
1083 (sort (delq nil
1084 (mapcar (lambda (w)
1085 (when (eq (window-frame w)
1086 (selected-frame))
1087 w))
1088 (ampc-normalize-windows)))
1089 (lambda (w1 w2)
1090 (loop for w in (window-list nil nil (frame-first-window))
1091 do (when (eq w w1)
1092 (return t))
1093 (when (eq w w2)
1094 (return nil)))))))
1095 (when windows
1096 (setf (window-dedicated-p (car windows)) nil)
1097 (loop for w in (cdr windows)
1098 do (delete-window w)))))
1099
1100 (defun ampc-tagger-tags-modified (tags new-tags)
1101 (loop with found-changed
1102 for (tag . value) in new-tags
1103 for prop = (assq tag tags)
1104 do (unless (equal (cdr prop) value)
1105 (setf (cdr prop) value
1106 found-changed t))
1107 finally return found-changed))
1108
1109 (defun ampc-change-view (view)
1110 (if (equal ampc-outstanding-commands '((idle nil)))
1111 (ampc-configure-frame (cddr view))
1112 (message "ampc is busy, cannot change window layout")))
1113
1114 (defun ampc-quote (string)
1115 (concat "\"" (replace-regexp-in-string "\"" "\\\"" string) "\""))
1116
1117 (defun ampc-in-ampc-p (&optional or-in-tagger)
1118 (or (when (ampc-on-p)
1119 ampc-type)
1120 (when or-in-tagger
1121 (memq (car ampc-type) '(files-list tagger)))))
1122
1123 (defun ampc-add-impl (&optional data)
1124 (ampc-on-files (lambda (file)
1125 (if (ampc-playlist)
1126 (ampc-send-command 'playlistadd
1127 '(:keep-prev t)
1128 (ampc-quote (ampc-playlist))
1129 file)
1130 (ampc-send-command 'add '(:keep-prev t) (ampc-quote file)))
1131 data)))
1132
1133 (defun ampc-on-files (func &optional data)
1134 (cond ((null data)
1135 (loop for d in (get-text-property (line-end-position) 'data)
1136 do (ampc-on-files func d)))
1137 ((avl-tree-p data)
1138 (avl-tree-mapc (lambda (e) (ampc-on-files func (cdr e))) data))
1139 ((stringp data)
1140 (funcall func data))
1141 (t
1142 (loop for d in (reverse data)
1143 do (ampc-on-files func (cdr (assoc "file" d)))))))
1144
1145 (defun ampc-skip (N)
1146 (ampc-send-command
1147 'play
1148 `(:callback ,(lambda ()
1149 (ampc-send-command 'status '(:front t))))
1150 (lambda ()
1151 (let ((song (cdr (assq 'song ampc-status)))
1152 (playlist-length (cdr (assq 'playlistlength ampc-status))))
1153 (unless (and song playlist-length)
1154 (throw 'skip nil))
1155 (max 0 (min (+ (string-to-number song) N)
1156 (1- (string-to-number playlist-length))))))))
1157
1158 (defun* ampc-find-current-song
1159 (limit &aux (point (point)) (song (cdr (assq 'song ampc-status))))
1160 (when (and song
1161 (<= (1- (line-number-at-pos (point)))
1162 (setf song (string-to-number song)))
1163 (>= (1- (line-number-at-pos limit)) song))
1164 (goto-char (point-min))
1165 (forward-line song)
1166 (save-restriction
1167 (narrow-to-region (max point (point)) (min limit (line-end-position)))
1168 (search-forward-regexp "\\(?1:\\(\\`\\*\\)?\\)\\(?2:.*\\)$"))))
1169
1170 (defun ampc-set-volume-impl (arg &optional func)
1171 (when arg
1172 (setf arg (prefix-numeric-value arg)))
1173 (ampc-send-command
1174 'setvol
1175 `(:callback ,(lambda ()
1176 (ampc-send-command 'status '(:front t))))
1177 (lambda ()
1178 (unless ampc-status
1179 (throw 'skip nil))
1180 (max (min (if func
1181 (funcall func
1182 (string-to-number
1183 (cdr (assq 'volume ampc-status)))
1184 (or arg ampc-volume-step))
1185 arg)
1186 100)
1187 0))))
1188
1189 (defun ampc-set-crossfade-impl (arg &optional func)
1190 (when arg
1191 (setf arg (prefix-numeric-value arg)))
1192 (ampc-send-command
1193 'crossfade
1194 `(:callback ,(lambda ()
1195 (ampc-send-command 'status '(:front t))))
1196 (lambda ()
1197 (unless ampc-status
1198 (throw 'skip nil))
1199 (max (if func
1200 (funcall func
1201 (string-to-number
1202 (cdr (assq 'xfade ampc-status)))
1203 (or arg ampc-crossfade-step))
1204 arg)
1205 0))))
1206
1207 (defun* ampc-tagger-make-backup (file)
1208 (unless ampc-tagger-backup-directory
1209 (return-from ampc-tagger-make-backup))
1210 (when (functionp ampc-tagger-backup-directory)
1211 (funcall ampc-tagger-backup-directory file)
1212 (return-from ampc-tagger-make-backup))
1213 (unless (file-directory-p ampc-tagger-backup-directory)
1214 (make-directory ampc-tagger-backup-directory t))
1215 (let* ((real-file
1216 (loop with real-file = file
1217 for target = (file-symlink-p real-file)
1218 while target
1219 do (setf real-file (expand-file-name
1220 target (file-name-directory real-file)))
1221 finally return real-file))
1222 (target
1223 (loop with base = (file-name-nondirectory real-file)
1224 for i from 1
1225 for file = (expand-file-name
1226 (concat base ".~"
1227 (int-to-string i)
1228 "~")
1229 ampc-tagger-backup-directory)
1230 while (file-exists-p file)
1231 finally return file)))
1232 (ampc-tagger-log "\tBackup file: " (abbreviate-file-name target) "\n")
1233 (copy-file real-file target nil t)))
1234
1235 (defun* ampc-move (N &aux with-marks entries-to-move (up (< N 0)))
1236 (save-excursion
1237 (goto-char (point-min))
1238 (loop while (search-forward-regexp "^* " nil t)
1239 do (push (point) entries-to-move)))
1240 (if entries-to-move
1241 (setf with-marks t)
1242 (push (point) entries-to-move))
1243 (when (save-excursion
1244 (loop with max = (1- (count-lines (point-min) (point-max)))
1245 for p in entries-to-move
1246 do (goto-char p)
1247 for line = (+ (1- (line-number-at-pos)) N)
1248 always (and (>= line 0) (<= line max))))
1249 (when up
1250 (setf entries-to-move (nreverse entries-to-move)))
1251 (when with-marks
1252 (ampc-unmark-all))
1253 (loop for p in entries-to-move
1254 do (goto-char p)
1255 for line = (1- (line-number-at-pos))
1256 do (if (and (not (eq (car ampc-type) 'current-playlist))
1257 (ampc-playlist))
1258 (ampc-send-command 'playlistmove
1259 '(:keep-prev t)
1260 (ampc-quote (ampc-playlist))
1261 line
1262 (+ line N))
1263 (ampc-send-command 'move '(:keep-prev t) line (+ line N))))
1264 (if with-marks
1265 (loop for p in (nreverse entries-to-move)
1266 do (goto-char p)
1267 (forward-line N)
1268 (save-excursion
1269 (ampc-mark-impl t 1))
1270 (ampc-align-point))
1271 (forward-line N)
1272 (ampc-align-point))))
1273
1274 (defun ampc-toggle-state (state arg)
1275 (when (or arg ampc-status)
1276 (ampc-send-command
1277 state
1278 nil
1279 (cond ((null arg)
1280 (if (equal (cdr (assq state ampc-status)) "1")
1281 0
1282 1))
1283 ((> (prefix-numeric-value arg) 0) 1)
1284 (t 0)))))
1285
1286 (defun ampc-playlist (&optional at-point)
1287 (ampc-with-buffer 'playlists
1288 (if (and (not at-point)
1289 (search-forward-regexp "^* \\(.*\\)$" nil t))
1290 (let ((result (match-string 1)))
1291 (set-text-properties 0 (length result) nil result)
1292 result)
1293 (unless (eobp)
1294 (buffer-substring-no-properties
1295 (+ (line-beginning-position) 2)
1296 (line-end-position))))))
1297
1298 (defun* ampc-mark-impl (select N &aux result (inhibit-read-only t))
1299 (when (eq (car ampc-type) 'playlists)
1300 (assert (or (not select) (null N) (eq N 1)))
1301 (ampc-with-buffer 'playlists
1302 (loop while (search-forward-regexp "^\\* " nil t)
1303 do (replace-match " " nil nil))))
1304 (loop repeat (or N 1)
1305 until (eobp)
1306 do (move-beginning-of-line nil)
1307 (delete-char 1)
1308 (insert (if select "*" " "))
1309 (setf result (ampc-next-line nil)))
1310 (ampc-post-mark-change-update)
1311 result)
1312
1313 (defun ampc-post-mark-change-update ()
1314 (ecase (car ampc-type)
1315 ((current-playlist playlist outputs))
1316 (playlists
1317 (ampc-update-playlist))
1318 ((song tag)
1319 (loop
1320 for w in
1321 (loop for w on (ampc-normalize-windows)
1322 thereis (when (or (eq (car w) (selected-window))
1323 (and (eq (car ampc-type) 'tag)
1324 (eq (with-current-buffer
1325 (window-buffer (car w))
1326 (car ampc-type))
1327 'song)))
1328 (cdr w)))
1329 do (with-current-buffer (window-buffer w)
1330 (when (memq (car ampc-type) '(song tag))
1331 (ampc-set-dirty t))))
1332 (ampc-fill-tag-song))
1333 (files-list
1334 (ampc-tagger-update))))
1335
1336 (defun* ampc-tagger-get-values (tag all-files &aux result)
1337 (ampc-with-buffer 'files-list
1338 no-se
1339 (save-excursion
1340 (macrolet
1341 ((add-file
1342 ()
1343 `(let ((value (cdr (assq tag (get-text-property (point) 'data)))))
1344 (unless (member value result)
1345 (push value result)))))
1346 (if all-files
1347 (loop until (eobp)
1348 initially do (goto-char (point-min))
1349 (ampc-align-point)
1350 do (add-file)
1351 until (ampc-next-line))
1352 (ampc-with-selection nil
1353 (add-file))))))
1354 result)
1355
1356 (defun ampc-tagger-update ()
1357 (ampc-with-buffer 'tagger
1358 (loop
1359 while (search-forward-regexp (concat "^[ \t]*\\("
1360 (mapconcat 'symbol-name
1361 ampc-tagger-tags
1362 "\\|")
1363 "\\)[ \t]*:"
1364 "[ \t]*\\(<keep>[ \t]*?\\)"
1365 "\\(?:\n\\)?$")
1366 nil
1367 t)
1368 for tag = (intern (match-string 1))
1369 do (when (memq tag ampc-tagger-tags)
1370 (let ((values (save-match-data (ampc-tagger-get-values tag nil))))
1371 (when (eq (length values) 1)
1372 (replace-match (car values) nil t nil 2)))))))
1373
1374 (defun ampc-tagger-complete-tag ()
1375 (save-excursion
1376 (save-restriction
1377 (narrow-to-region (line-beginning-position) (line-end-position))
1378 (unless (search-backward-regexp "^.*:" nil t)
1379 (when (search-backward-regexp "\\(^\\|[ \t]\\).*" nil t)
1380 (when (looking-at "[ \t]")
1381 (forward-char 1))
1382 (list (point)
1383 (search-forward-regexp ":\\|$")
1384 (mapcar (lambda (tag) (concat (symbol-name tag) ":"))
1385 ampc-tagger-tags)))))))
1386
1387 (defun* ampc-tagger-complete-value (&aux tag)
1388 (save-excursion
1389 (save-restriction
1390 (narrow-to-region (line-beginning-position) (line-end-position))
1391 (save-excursion
1392 (unless (search-backward-regexp (concat "^[ \t]*\\("
1393 (mapconcat 'symbol-name
1394 ampc-tagger-tags
1395 "\\|")
1396 "\\)[ \t]*:")
1397 nil t)
1398 (return-from ampc-tagger-complete-tag))
1399 (setf tag (intern (match-string 1))))
1400 (save-excursion
1401 (search-backward-regexp "[: \t]")
1402 (forward-char 1)
1403 (list (point)
1404 (search-forward-regexp "[ \t]\\|$")
1405 (let ((values (cons "<keep>" (ampc-tagger-get-values
1406 tag
1407 ampc-tagger-completion-all-files))))
1408 (when (eq tag 'Genre)
1409 (loop for g in ampc-tagger-genres
1410 do (unless (member g values)
1411 (push g values))))
1412 values))))))
1413
1414 (defun ampc-align-point ()
1415 (unless (eobp)
1416 (move-beginning-of-line nil)
1417 (forward-char 2)
1418 (re-search-forward " *" nil t)))
1419
1420 (defun* ampc-pad (tabs &optional dont-honour-item-mode)
1421 (loop with new-tab-stop-list
1422 with offset-dec = (if (and (not dont-honour-item-mode)
1423 (derived-mode-p 'ampc-item-mode))
1424 2
1425 0)
1426 for tab in tabs
1427 for offset-cell on (if (derived-mode-p 'ampc-item-mode)
1428 tab-stop-list
1429 (cons 0 tab-stop-list))
1430 for offset = (car offset-cell)
1431 for props in (or (plist-get (cdr ampc-type) :properties)
1432 '(nil . nil))
1433 by (lambda (cell) (or (cdr cell) '(nil . nil)))
1434 do (decf offset offset-dec)
1435 with first = t
1436 with current-offset = 0
1437 when (<= current-offset offset)
1438 do (when (and (not first) (eq (- offset current-offset) 0))
1439 (incf offset))
1440 and concat (make-string (- offset current-offset) ? ) into result
1441 and do (setf current-offset offset)
1442 else
1443 concat " " into result
1444 and do (incf current-offset)
1445 end
1446 do (unless tab
1447 (setf tab ""))
1448 (when (and (plist-get (cdr props) :shrink)
1449 (cadr offset-cell)
1450 (>= (+ current-offset (length tab) 1) (- (cadr offset-cell)
1451 offset-dec)))
1452 (setf tab (concat (substring tab 0 (max (- (cadr offset-cell)
1453 offset-dec
1454 current-offset
1455 4)
1456 3))
1457 "...")))
1458 concat tab into result
1459 do (push (+ current-offset offset-dec) new-tab-stop-list)
1460 (incf current-offset (length tab))
1461 (setf first nil)
1462 finally return
1463 (if (equal (callf nreverse new-tab-stop-list) tab-stop-list)
1464 result
1465 (propertize result 'tab-stop-list new-tab-stop-list))))
1466
1467 (defun ampc-update-header ()
1468 (when (or (memq (car ampc-type) '(tag playlists))
1469 (plist-get (cdr ampc-type) :properties))
1470 (setf header-line-format
1471 (concat
1472 (make-string (floor (fringe-columns 'left t)) ? )
1473 (ecase (car ampc-type)
1474 (tag
1475 (concat " " (plist-get (cdr ampc-type) :tag)))
1476 (playlists
1477 " Playlists")
1478 (t
1479 (ampc-pad (loop for (name . props) in
1480 (plist-get (cdr ampc-type) :properties)
1481 collect (or (plist-get props :title) name))
1482 t)))))))
1483
1484 (defun ampc-set-dirty (tag-or-dirty &optional dirty)
1485 (if (or (null tag-or-dirty) (memq tag-or-dirty '(t erase)))
1486 (setf ampc-dirty tag-or-dirty)
1487 (loop for w in (ampc-normalize-windows)
1488 do (with-current-buffer (window-buffer w)
1489 (when (eq (car ampc-type) tag-or-dirty)
1490 (ampc-set-dirty dirty))))))
1491
1492 (defun ampc-update ()
1493 (if ampc-status
1494 (loop for w in (ampc-normalize-windows)
1495 do (with-current-buffer (window-buffer w)
1496 (when (and ampc-dirty (not (eq ampc-dirty 'keep-dirty)))
1497 (ecase (car ampc-type)
1498 (outputs
1499 (ampc-send-command 'outputs))
1500 (playlist
1501 (ampc-update-playlist))
1502 ((tag song)
1503 (if (assoc (ampc-tags) ampc-internal-db)
1504 (ampc-fill-tag-song)
1505 (push (cons (ampc-tags) nil) ampc-internal-db)
1506 (ampc-send-command 'listallinfo)))
1507 (status
1508 (ampc-send-command 'status)
1509 (ampc-send-command 'currentsong))
1510 (playlists
1511 (ampc-send-command 'listplaylists))
1512 (current-playlist
1513 (ampc-send-command 'playlistinfo))))))
1514 (ampc-send-command 'status)
1515 (ampc-send-command 'currentsong)))
1516
1517 (defun ampc-update-playlist ()
1518 (ampc-with-buffer 'playlists
1519 (if (search-forward-regexp "^\\* " nil t)
1520 (ampc-send-command 'listplaylistinfo
1521 nil
1522 (get-text-property (point) 'data))
1523 (ampc-with-buffer 'playlist
1524 (erase-buffer)
1525 (ampc-set-dirty nil)))))
1526
1527 (defun ampc-send-command-impl (command)
1528 (when ampc-debug
1529 (message "ampc: -> %s" command))
1530 (when (ampc-on-p)
1531 (process-send-string ampc-connection (concat command "\n"))))
1532
1533 (defun* ampc-send-command (command &optional props &rest args)
1534 (destructuring-bind (&key (front nil) (keep-prev nil) (full-remove nil)
1535 (remove-other nil) &allow-other-keys
1536 &aux idle)
1537 props
1538 (when (and (not keep-prev)
1539 (eq (caar ampc-outstanding-commands) command)
1540 (equal (cddar ampc-outstanding-commands) args))
1541 (return-from ampc-send-command))
1542 (unless ampc-working-timer
1543 (setf ampc-yield 0
1544 ampc-working-timer (run-at-time nil 0.1 'ampc-yield)))
1545 (when (equal (caar ampc-outstanding-commands) 'idle)
1546 (pop ampc-outstanding-commands)
1547 (setf idle t))
1548 (when (and (not keep-prev) (cdr ampc-outstanding-commands))
1549 (setf (cdr ampc-outstanding-commands)
1550 (loop for other-cmd in (cdr ampc-outstanding-commands)
1551 unless (and (memq (car other-cmd) (list command remove-other))
1552 (or (not full-remove)
1553 (progn
1554 (assert (null remove-other))
1555 (equal (cddr other-cmd) args))))
1556 collect other-cmd
1557 end)))
1558 (setf command (apply 'list command props args))
1559 (if front
1560 (push command ampc-outstanding-commands)
1561 (setf ampc-outstanding-commands
1562 (nconc ampc-outstanding-commands
1563 (list command))))
1564 (when idle
1565 (push '(noidle nil) ampc-outstanding-commands)
1566 (ampc-send-command-impl "noidle"))))
1567
1568 (defun ampc-send-next-command ()
1569 (loop while ampc-outstanding-commands
1570 for command =
1571 (loop for command = (car ampc-outstanding-commands)
1572 for command-id = (replace-regexp-in-string
1573 "^.*?-" ""
1574 (symbol-name (car command)))
1575 thereis
1576 (catch 'skip
1577 (ampc-send-command-impl
1578 (concat command-id
1579 (loop for a in (cddr command)
1580 concat " "
1581 do (when (functionp a)
1582 (callf funcall a))
1583 concat (etypecase a
1584 (integer (number-to-string a))
1585 (string a)))))
1586 (let ((callback (plist-get (cadar ampc-outstanding-commands)
1587 :callback))
1588 (old-head (pop ampc-outstanding-commands)))
1589 (when callback (funcall callback))
1590 (push old-head ampc-outstanding-commands))
1591 command-id)
1592 do (pop ampc-outstanding-commands)
1593 while ampc-outstanding-commands)
1594 while command
1595 while (let ((member (memq (intern command) ampc-synchronous-commands)))
1596 (if member
1597 (not (eq (car ampc-synchronous-commands) t))
1598 (eq (car ampc-synchronous-commands) t)))
1599 do (loop with head = ampc-outstanding-commands
1600 with ampc-no-implicit-next-dispatch = t
1601 with ampc-yield-redisplay = t
1602 while (ampc-on-p)
1603 while (eq head ampc-outstanding-commands)
1604 do (accept-process-output ampc-connection 0 100)))
1605 (unless ampc-outstanding-commands
1606 (when ampc-working-timer
1607 (cancel-timer ampc-working-timer)
1608 (setf ampc-yield nil
1609 ampc-working-timer nil)
1610 (ampc-fill-status))
1611 (setf ampc-outstanding-commands '((idle nil)))
1612 (ampc-send-command-impl "idle")))
1613
1614 (defun ampc-tree< (a b)
1615 (string< (car a) (car b)))
1616
1617 (defun ampc-create-tree ()
1618 (avl-tree-create 'ampc-tree<))
1619
1620 (defsubst ampc-extract (regexp)
1621 (goto-char (point-min))
1622 (when (search-forward-regexp regexp nil t)
1623 (match-string 1)))
1624
1625 (defsubst ampc-clean-tag (tag value)
1626 (if value
1627 (let ((func (cdr (assoc tag ampc-tag-transform-funcs))))
1628 (if func
1629 (funcall func value)
1630 value))
1631 (unless (equal tag "Track")
1632 "[Not Specified]")))
1633
1634 (defun ampc-insert (element data &optional cmp cmp-data)
1635 (goto-char (point-min))
1636 (unless cmp-data
1637 (setf cmp-data data))
1638 (let ((action
1639 (if (functionp cmp)
1640 (loop until (eobp)
1641 for tp = (get-text-property (+ (point) 2) 'cmp-data)
1642 thereis (let ((r (funcall cmp cmp-data tp)))
1643 (if (symbolp r)
1644 r
1645 (forward-line r)
1646 nil))
1647 finally return 'insert)
1648 (loop with stringp-cmp-data = (stringp cmp-data)
1649 with min = 1
1650 with max = (1+ (count-lines (point-min) (point-max)))
1651 with at-min = t
1652 do (when (< (- max min) 20)
1653 (unless at-min
1654 (forward-line (- min max)))
1655 (return (loop repeat (- max min)
1656 for tp = (get-text-property (+ (point) 2)
1657 'cmp-data)
1658 thereis
1659 (if (equal tp cmp-data)
1660 'update
1661 (unless (if stringp-cmp-data
1662 (string< tp cmp-data)
1663 (string<
1664 (buffer-substring-no-properties
1665 (+ (point) 2)
1666 (line-end-position))
1667 element))
1668 'insert))
1669 do (forward-line)
1670 finally return 'insert)))
1671 do (forward-line (funcall (if at-min '+ '-) (/ (- max min) 2)))
1672 for tp = (get-text-property (+ (point) 2) 'cmp-data)
1673 thereis (when (equal tp cmp-data) 'update)
1674 do (if (setf at-min (if stringp-cmp-data
1675 (string< tp cmp-data)
1676 (string< (buffer-substring-no-properties
1677 (+ (point) 2)
1678 (line-end-position))
1679 element)))
1680 (incf min (floor (/ (- max min) 2.0)))
1681 (decf max (floor (/ (- max min) 2.0))))
1682 finally return 'insert))))
1683 (ecase action
1684 (insert
1685 (insert (propertize (concat " " element "\n")
1686 'data (if (eq cmp t) (list data) data)
1687 'cmp-data cmp-data)))
1688 ((update overwrite)
1689 (remove-text-properties (point) (1+ (point)) '(not-updated))
1690 (when (or (eq ampc-dirty 'erase) (eq action 'overwrite))
1691 (let ((origin (point)))
1692 (forward-char 2)
1693 (kill-line 1)
1694 (insert element "\n")
1695 (goto-char origin)))
1696 (let ((next (1+ (line-end-position))))
1697 (put-text-property (point) next 'cmp-data cmp-data)
1698 (put-text-property
1699 (point) next
1700 'data (cond ((eq cmp t)
1701 (let ((rest (get-text-property (point) 'data)))
1702 (if (memq data rest)
1703 rest
1704 (cons data rest))))
1705 (t data))))
1706 (eq (char-after) ?*)))))
1707
1708 (defun ampc-fill-tag (trees)
1709 (put-text-property (point-min) (point-max) 'data nil)
1710 (loop with new-trees
1711 for tree in trees
1712 do (when tree
1713 (avl-tree-mapc
1714 (lambda (e)
1715 (when (ampc-insert (car e) (cdr e) t (car e))
1716 (push (cdr e) new-trees)))
1717 tree))
1718 finally return new-trees))
1719
1720 (defun ampc-fill-song (trees)
1721 (loop
1722 for songs in trees
1723 do (loop for song in songs
1724 do (ampc-insert
1725 (ampc-pad
1726 (loop for (p . v) in (plist-get (cdr ampc-type) :properties)
1727 collect (cdr (assoc p song))))
1728 `((,song))))))
1729
1730 (defsubst ampc-narrow-entry (delimiter-regexp)
1731 (let ((result))
1732 (narrow-to-region
1733 (line-beginning-position)
1734 (or (save-excursion
1735 (goto-char (line-end-position))
1736 (when (search-forward-regexp delimiter-regexp nil t)
1737 (setf result (point))
1738 (1- (line-beginning-position))))
1739 (point-max)))
1740 result))
1741
1742 (defun ampc-fill-playlist ()
1743 (ampc-fill-skeleton 'playlist
1744 (let ((index 0))
1745 (ampc-iterate-source-output "file" (file)
1746 (loop for (tag . tag-regexp) in tags
1747 collect (ampc-clean-tag tag (ampc-extract tag-regexp)))
1748 `(("file" . ,file)
1749 (index . ,(1- (incf index))))
1750 'ampc-int-insert-cmp
1751 index))))
1752
1753 (defun ampc-fill-outputs ()
1754 (ampc-fill-skeleton 'outputs
1755 (ampc-iterate-source-output "outputid" (outputid outputenabled)
1756 (loop for (tag . tag-regexp) in tags
1757 collect (ampc-clean-tag tag (ampc-extract tag-regexp)))
1758 `(("outputid" . ,outputid)
1759 ("outputenabled" . ,outputenabled)))))
1760
1761 (defun* ampc-mini-impl (&aux songs)
1762 (ampc-iterate-source
1763 nil
1764 "file"
1765 (Title
1766 Artist
1767 (Pos (string-to-number (ampc-extract (ampc-extract-regexp "Pos")))))
1768 (let ((entry (cons (concat Title
1769 (when Artist
1770 (concat " - " Artist)))
1771 Pos)))
1772 (loop with mentry = (cons (car entry) (cdr entry))
1773 for index from 2
1774 while (assoc (car mentry) songs)
1775 do (setf (car mentry) (concat (car entry)
1776 " (" (int-to-string index) ")"))
1777 finally do (push mentry songs))))
1778 (unless songs
1779 (message "No song in the playlist")
1780 (return-from ampc-mini-impl))
1781 (let ((song (assoc (let ((inhibit-quit t))
1782 (prog1
1783 (with-local-quit
1784 (completing-read "Song to play: " songs nil t))
1785 (setf quit-flag nil)))
1786 songs)))
1787 (when song
1788 (ampc-play-this (cdr song)))))
1789
1790 (defun ampc-fill-current-playlist ()
1791 (ampc-fill-skeleton 'current-playlist
1792 (ampc-iterate-source-output
1793 "file"
1794 (file (pos (string-to-number (ampc-extract
1795 (ampc-extract-regexp "Pos")))))
1796 (loop for (tag . tag-regexp) in tags
1797 collect (ampc-clean-tag tag (ampc-extract tag-regexp)))
1798 `(("file" . ,file)
1799 ("Pos" . ,pos))
1800 'ampc-int-insert-cmp
1801 pos)))
1802
1803 (defun ampc-fill-playlists ()
1804 (ampc-fill-skeleton 'playlists
1805 (with-current-buffer data-buffer
1806 (loop while (search-forward-regexp "^playlist: \\(.*\\)$" nil t)
1807 for playlist = (match-string 1)
1808 do (ampc-with-buffer 'playlists
1809 (ampc-insert playlist playlist)))))
1810 (ampc-set-dirty 'playlist t)
1811 (ampc-update))
1812
1813 (defun ampc-yield ()
1814 (incf ampc-yield)
1815 (ampc-fill-status)
1816 (when ampc-yield-redisplay
1817 (redisplay t)))
1818
1819 (defun ampc-fill-status ()
1820 (ampc-with-buffer 'status
1821 (erase-buffer)
1822 (funcall (or (plist-get (cadr ampc-type) :filler)
1823 (lambda (_)
1824 (insert (ampc-status t) "\n")))
1825 ampc-status)
1826 (ampc-set-dirty nil)))
1827
1828 (defun ampc-fill-tag-song ()
1829 (loop
1830 with trees = (list (cdr (assoc (ampc-tags) ampc-internal-db)))
1831 for type in '(tag song)
1832 do
1833 (loop
1834 for w in (ampc-normalize-windows)
1835 do
1836 (with-current-buffer (window-buffer w)
1837 (when (eq (car ampc-type) type)
1838 (if ampc-dirty
1839 (if (not trees)
1840 (progn
1841 (let ((inhibit-read-only t))
1842 (erase-buffer))
1843 (ampc-set-dirty nil))
1844 (ampc-fill-skeleton w
1845 (if (eq type 'tag)
1846 (setf trees (ampc-fill-tag trees))
1847 (ampc-fill-song trees))))
1848 (setf trees nil)
1849 (save-excursion
1850 (goto-char (point-min))
1851 (loop while (search-forward-regexp "^* " nil t)
1852 do (callf append trees
1853 (get-text-property (point) 'data))))))))))
1854
1855 (defun ampc-transform-track (track)
1856 (when (eq (length track) 1)
1857 (setf track (concat "0" track)))
1858 track)
1859
1860 (defun* ampc-transform-time (data &aux (time (string-to-number data)))
1861 (concat (number-to-string (/ time 60))
1862 ":"
1863 (when (< (% time 60) 10)
1864 "0")
1865 (number-to-string (% time 60))))
1866
1867 (defun ampc-handle-idle ()
1868 (loop until (eobp)
1869 for subsystem = (buffer-substring (point) (line-end-position))
1870 do (when (string-match "^changed: \\(.*\\)$" subsystem)
1871 (case (intern (match-string 1 subsystem))
1872 (database
1873 (setf ampc-internal-db (list (cons (ampc-tags) nil)))
1874 (ampc-set-dirty 'tag 'keep-dirty)
1875 (ampc-set-dirty 'song 'keep-dirty)
1876 (ampc-send-command 'listallinfo))
1877 (output
1878 (ampc-set-dirty 'outputs t))
1879 ((player options mixer)
1880 (setf ampc-status nil)
1881 (ampc-set-dirty 'status t))
1882 (stored_playlist
1883 (ampc-set-dirty 'playlists t))
1884 (playlist
1885 (ampc-set-dirty 'current-playlist t)
1886 (ampc-set-dirty 'status t))))
1887 (forward-line))
1888 (ampc-update))
1889
1890 (defun ampc-handle-setup (status)
1891 (unless (and (string-match "^ MPD \\(.+\\)\\.\\(.+\\)\\.\\(.+\\)$"
1892 status)
1893 (let ((version-a (string-to-number (match-string 1 status)))
1894 (version-b (string-to-number (match-string 2 status)))
1895 ;; (version-c (string-to-number (match-string 2 status)))
1896 )
1897 (or (> version-a 0)
1898 (>= version-b 15))))
1899 (error (concat "Your version of MPD is not supported. "
1900 "ampc supports MPD protocol version 0.15.0 "
1901 "and later"))))
1902
1903 (defun ampc-fill-internal-db (running)
1904 (loop with tree = (assoc (ampc-tags) ampc-internal-db)
1905 with tags =
1906 (loop for w in (ampc-normalize-windows)
1907 for props = (with-current-buffer (window-buffer w)
1908 (when (eq (car ampc-type) 'tag)
1909 (ampc-set-dirty t)
1910 (plist-get (cdr ampc-type) :tag)))
1911 when props
1912 collect props
1913 end)
1914 with song-props = (ampc-with-buffer 'song
1915 (ampc-set-dirty t)
1916 (plist-get (cdr ampc-type) :properties))
1917 for origin = (and (search-forward-regexp "^file: " nil t)
1918 (line-beginning-position))
1919 then next
1920 while origin
1921 do (goto-char (1+ origin))
1922 for next = (and (search-forward-regexp "^file: " nil t)
1923 (line-beginning-position))
1924 while (or (not running) next)
1925 do (save-restriction
1926 (narrow-to-region origin (or next (point-max)))
1927 (ampc-fill-internal-db-entry tree tags song-props))
1928 (when running
1929 (delete-region origin next)
1930 (setf next origin))))
1931
1932 (defun ampc-tags ()
1933 (loop for w in (ampc-normalize-windows)
1934 for tag = (with-current-buffer (window-buffer w)
1935 (when (eq (car ampc-type) 'tag)
1936 (plist-get (cdr ampc-type) :tag)))
1937 when tag
1938 collect tag
1939 end))
1940
1941 (defun ampc-fill-internal-db-entry (tree tags song-props)
1942 (loop for tag in tags
1943 for data = (ampc-clean-tag tag (ampc-extract (ampc-extract-regexp tag)))
1944 do (unless (cdr tree)
1945 (setf (cdr tree) (ampc-create-tree)))
1946 (setf tree (avl-tree-enter (cdr tree)
1947 (cons data nil)
1948 (lambda (_ match)
1949 match))))
1950 (push (cons (cons "file" (ampc-extract (ampc-extract-regexp "file")))
1951 (loop for p in song-props
1952 for data = (ampc-clean-tag (car p)
1953 (ampc-extract
1954 (ampc-extract-regexp (car p))))
1955 when data
1956 collect (cons (car p) data)
1957 end))
1958 (cdr tree)))
1959
1960 (defun ampc-fill-status-var (tags)
1961 (loop for k in tags
1962 for v = (ampc-extract (ampc-extract-regexp k))
1963 for s = (intern k)
1964 do (if v
1965 (setf (cdr (or (assq s ampc-status)
1966 (car (push (cons s nil) ampc-status))))
1967 v)
1968 (callf2 assq-delete-all s ampc-status))))
1969
1970 (defun ampc-handle-current-song ()
1971 (ampc-fill-status-var (append ampc-status-tags '("Artist" "Title" "file")))
1972 (ampc-fill-status)
1973 (run-hook-with-args ampc-status-changed-hook ampc-status))
1974
1975 (defun ampc-handle-status ()
1976 (ampc-fill-status-var '("volume" "repeat" "random" "consume" "xfade" "state"
1977 "song" "playlistlength"))
1978 (ampc-with-buffer 'current-playlist
1979 (when ampc-highlight-current-song-mode
1980 (font-lock-fontify-buffer)))
1981 (run-hook-with-args ampc-status-changed-hook ampc-status))
1982
1983 (defun ampc-handle-update ()
1984 (message "Database update started"))
1985
1986 (defun ampc-handle-command (status)
1987 (cond
1988 ((eq status 'error)
1989 (pop ampc-outstanding-commands))
1990 ((eq status 'running)
1991 (case (caar ampc-outstanding-commands)
1992 (listallinfo (ampc-fill-internal-db t))))
1993 (t
1994 (let ((command (pop ampc-outstanding-commands)))
1995 (case (car command)
1996 (idle
1997 (ampc-handle-idle))
1998 (setup
1999 (ampc-handle-setup status))
2000 (currentsong
2001 (ampc-handle-current-song))
2002 (status
2003 (ampc-handle-status))
2004 (update
2005 (ampc-handle-update))
2006 (listplaylistinfo
2007 (ampc-fill-playlist))
2008 (listplaylists
2009 (ampc-fill-playlists))
2010 (playlistinfo
2011 (ampc-fill-current-playlist))
2012 (mini-playlistinfo
2013 (ampc-mini-impl))
2014 (mini-currentsong
2015 (ampc-status))
2016 (shuffle-listplaylistinfo
2017 (ampc-shuffle-playlist (plist-get (cadr command) :playlist)))
2018 (listallinfo
2019 (ampc-handle-listallinfo))
2020 (outputs
2021 (ampc-fill-outputs))))
2022 (unless ampc-outstanding-commands
2023 (ampc-update)))))
2024
2025 (defun* ampc-shuffle-playlist (playlist &aux songs)
2026 (ampc-iterate-source nil "file" (file)
2027 (push (cons file (random)) songs))
2028 (ampc-send-command 'playlistclear '(:full-remove t) (ampc-quote playlist))
2029 (loop for file in (mapcar 'car (sort songs
2030 (lambda (a b) (< (cdr a) (cdr b)))))
2031 do (ampc-send-command 'playlistadd
2032 '(:keep-prev t)
2033 (ampc-quote playlist)
2034 file)))
2035
2036
2037 (defun ampc-handle-listallinfo ()
2038 (ampc-fill-internal-db nil)
2039 (ampc-set-dirty 'tag t)
2040 (ampc-set-dirty 'song t))
2041
2042 (defun ampc-filter (_process string)
2043 (assert (buffer-live-p (process-buffer ampc-connection)))
2044 (with-current-buffer (process-buffer ampc-connection)
2045 (when string
2046 (when (and ampc-debug (not (eq ampc-debug t)))
2047 (message "ampc: <- %s" string))
2048 (goto-char (process-mark ampc-connection))
2049 (insert string)
2050 (set-marker (process-mark ampc-connection) (point)))
2051 (save-excursion
2052 (goto-char (point-min))
2053 (let ((success))
2054 (if (or (progn
2055 (when (search-forward-regexp
2056 "^ACK \\[\\(.*\\)\\] {.*} \\(.*\\)\n\\'"
2057 nil
2058 t)
2059 (message "ampc command error: %s (%s; %s)"
2060 (match-string 2)
2061 (match-string 1)
2062 (funcall (if ampc-debug 'identity 'car)
2063 (car ampc-outstanding-commands)))
2064 t))
2065 (when (search-forward-regexp "^OK\\(.*\\)\n\\'" nil t)
2066 (setf success t)))
2067 (progn
2068 (let ((match-end (match-end 0)))
2069 (save-restriction
2070 (narrow-to-region (point-min) match-end)
2071 (goto-char (point-min))
2072 (ampc-handle-command (if success (match-string 1) 'error)))
2073 (delete-region (point-min) match-end))
2074 (unless ampc-no-implicit-next-dispatch
2075 (ampc-send-next-command))))
2076 (ampc-handle-command 'running)))))
2077
2078 (defun* ampc-set-tab-offsets
2079 (&rest properties &aux (min 2) (optional-padding 0))
2080 (unless properties
2081 (return-from ampc-set-tab-offsets))
2082 (set (make-local-variable 'tab-stop-list) nil)
2083 (loop for (title . props) in properties
2084 for min- = (plist-get props :min)
2085 do (incf min (or (plist-get props :width) min-))
2086 (when min-
2087 (incf optional-padding (- (plist-get props :max) min-))))
2088 (loop for (title . props) in properties
2089 with offset = 2
2090 do (push offset tab-stop-list)
2091 (incf offset (or (plist-get props :width)
2092 (let ((min- (plist-get props :min))
2093 (max (plist-get props :max)))
2094 (if (>= min (window-width))
2095 min-
2096 (min max
2097 (+ min-
2098 (floor (* (/ (float (- max min-))
2099 optional-padding)
2100 (- (window-width)
2101 min))))))))))
2102 (callf nreverse tab-stop-list))
2103
2104 (defun* ampc-configure-frame-1 (split &aux (split-type (car split)))
2105 (if (memq split-type '(vertical horizontal))
2106 (let* ((sizes))
2107 (loop with length = (if (eq split-type 'horizontal)
2108 (window-total-width)
2109 (window-total-height))
2110 with rest = length
2111 with rest-car
2112 for (size . subsplit) in (cdr split)
2113 do (if (equal size 1.0)
2114 (progn (push t sizes)
2115 (setf rest-car sizes))
2116 (let ((l (if (integerp size) size (round (* size length)))))
2117 (decf rest l)
2118 (push l sizes)))
2119 finally do (setf (car rest-car) rest))
2120 (let ((first-window (selected-window)))
2121 (callf nreverse sizes)
2122 (loop for size in (copy-sequence sizes)
2123 for window on (cdr sizes)
2124 do (select-window
2125 (setf (car window)
2126 (split-window nil size (eq split-type 'horizontal)))))
2127 (setf (car sizes) first-window))
2128 (loop for subsplit in (cdr split)
2129 for window in sizes
2130 with result
2131 do (with-selected-window window
2132 (setf result
2133 (or (ampc-configure-frame-1 (cdr subsplit)) result)))
2134 finally return result))
2135 (setf (window-dedicated-p (selected-window)) nil)
2136 (pop-to-buffer-same-window
2137 (get-buffer-create
2138 (concat "*"
2139 (mapconcat (lambda (s) (concat (upcase (substring s 0 1))
2140 (substring s 1)))
2141 (if (memq split-type '(tag song))
2142 (list (or (plist-get (cdr split) :tag) "song"))
2143 (split-string (symbol-name split-type) "-"))
2144 " ")
2145 "*")))
2146 (if (memq split-type '(tag song))
2147 (ampc-tag-song-mode)
2148 (let ((mode (intern (concat "ampc-" (symbol-name split-type) "-mode"))))
2149 (unless (fboundp mode)
2150 (setf mode 'ampc-mode))
2151 (unless (eq major-mode 'mode)
2152 (funcall mode))))
2153 (destructuring-bind
2154 (&key (properties nil) (dedicated t) (mode-line t) &allow-other-keys)
2155 (cdr split)
2156 (apply 'ampc-set-tab-offsets properties)
2157 (setf ampc-type split
2158 (window-dedicated-p (selected-window)) dedicated
2159 mode-line-format (when mode-line
2160 (default-value 'mode-line-format))))
2161 (set (make-local-variable 'mode-line-buffer-identification)
2162 '(:eval (let ((result
2163 (concat (car-safe (propertized-buffer-identification
2164 (buffer-name)))
2165 (when ampc-dirty
2166 " [Updating...]"))))
2167 (if (< (length result) 12)
2168 (concat result (make-string (- 12 (length result)) ? ))
2169 result))))
2170 (ampc-update-header)
2171 (add-to-list 'ampc-all-buffers (current-buffer))
2172 (push (cons (or (plist-get (cdr split) :id) 9999) (selected-window))
2173 ampc-windows)
2174 (ampc-set-dirty t)
2175 (when (plist-get (cdr split) :select)
2176 (selected-window))))
2177
2178 (defun* ampc-configure-frame
2179 (split &optional no-update &aux (old-selection ampc-type) old-window-starts)
2180 (loop for w in (ampc-normalize-windows)
2181 do (with-selected-window w
2182 (with-current-buffer (window-buffer w)
2183 (push (cons (current-buffer) (window-start))
2184 old-window-starts))))
2185 (if (not ampc-use-full-frame)
2186 (ampc-restore-window-configuration)
2187 (setf (window-dedicated-p (selected-window)) nil)
2188 (delete-other-windows))
2189 (setf ampc-windows nil)
2190 (let ((select-window (ampc-configure-frame-1 split)))
2191 (setf ampc-windows
2192 (mapcar (lambda (window)
2193 (cons window (window-buffer window)))
2194 (mapcar 'cdr (sort ampc-windows
2195 (lambda (a b) (< (car a) (car b)))))))
2196 (loop for w in (ampc-normalize-windows)
2197 do (with-selected-window w
2198 (let ((old-window-start (cdr (assq (current-buffer)
2199 old-window-starts))))
2200 (when old-window-start
2201 (set-window-start nil old-window-start)))
2202 (when (and (derived-mode-p 'ampc-item-mode)
2203 (> (length tab-stop-list) 1))
2204 (ampc-set-dirty 'erase))))
2205 (select-window (or (loop for w in (ampc-normalize-windows)
2206 thereis
2207 (when (equal (with-current-buffer (window-buffer w)
2208 ampc-type)
2209 old-selection)
2210 w))
2211 select-window
2212 (selected-window))))
2213 (unless no-update
2214 (ampc-update)))
2215
2216 (defun ampc-tagger-rename-artist-title (_changed-tags data)
2217 "Rename music file according to its tags.
2218 This function is meant to be inserted into
2219 `ampc-tagger-stored-hook'. The new file name is
2220 `Artist'_-_`Title'.`extension'. Characters within `Artist' and
2221 `Title' that are not alphanumeric are substituted with underscore."
2222 (let* ((artist (replace-regexp-in-string
2223 "[^a-zA-Z0-9]" "_"
2224 (or (cdr (assq 'Artist (cdr data))) "")))
2225 (title (replace-regexp-in-string
2226 "[^a-zA-Z0-9]" "_"
2227 (or (cdr (assq 'Title (cdr data))) "")))
2228 (new-file
2229 (expand-file-name (replace-regexp-in-string
2230 "_\\(_\\)+"
2231 "_"
2232 (concat artist
2233 (when (and (> (length artist) 0)
2234 (> (length title) 0))
2235 "_-_")
2236 title
2237 (file-name-extension (car data) t)))
2238 (file-name-directory (car data)))))
2239 (unless (equal (car data) new-file)
2240 (ampc-tagger-log "Renaming file " (abbreviate-file-name (car data))
2241 " to " (abbreviate-file-name new-file) "\n")
2242 (rename-file (car data) new-file)
2243 (setf (car data) new-file))))
2244
2245 ;;; *** interactives
2246 (defun ampc-tagger-completion-at-point (&optional all-files)
2247 "Perform completion at point via `completion-at-point'.
2248 If optional prefix argument ALL-FILES is non-nil, use all files
2249 within the files list buffer as source for completion. The
2250 default behaviour is to use only the selected ones."
2251 (interactive "P")
2252 (let ((ampc-tagger-completion-all-files all-files))
2253 (completion-at-point)))
2254
2255 (defun ampc-tagger-reset (&optional reset-all-tags)
2256 "Reset all tag values within the tagger, based on the selection of files.
2257 If optional prefix argument RESET-ALL-TAGS is non-nil, restore
2258 all tags."
2259 (interactive "P")
2260 (when reset-all-tags
2261 (ampc-with-buffer 'tagger
2262 no-se
2263 (erase-buffer)
2264 (loop for tag in ampc-tagger-tags
2265 do (insert (ampc-pad (list (concat (symbol-name tag) ":") "dummy"))
2266 "\n"))
2267 (goto-char (point-min))
2268 (re-search-forward ":\\( \\)+")))
2269 (ampc-with-buffer 'tagger
2270 (loop while (search-forward-regexp
2271 (concat "^\\([ \t]*\\)\\("
2272 (mapconcat 'symbol-name ampc-tagger-tags "\\|")
2273 "\\)\\([ \t]*\\):\\([ \t]*.*\\)$")
2274 nil
2275 t)
2276 do (replace-match "" nil nil nil 1)
2277 (replace-match "" nil nil nil 3)
2278 (replace-match (concat (make-string (- (car tab-stop-list)
2279 (1+ (length (match-string 2))))
2280 ? )
2281 "<keep>")
2282 nil nil nil 4)))
2283 (ampc-tagger-update)
2284 (ampc-with-buffer 'tagger
2285 no-se
2286 (when (looking-at "[ \t]+")
2287 (goto-char (match-end 0)))))
2288
2289 (defun* ampc-tagger-save (&optional quit &aux tags)
2290 "Save tags.
2291 If optional prefix argument QUIT is non-nil, quit tagger
2292 afterwards. If the numeric value of QUIT is 16, quit tagger and
2293 do not trigger a database update"
2294 (interactive "P")
2295 (ampc-with-buffer 'tagger
2296 (loop do (loop until (eobp)
2297 while (looking-at "^[ \t]*$")
2298 do (forward-line))
2299 until (eobp)
2300 do (unless (and (looking-at
2301 (concat "^[ \t]*\\("
2302 (mapconcat 'symbol-name
2303 ampc-tagger-tags
2304 "\\|")
2305 "\\)[ \t]*:"
2306 "[ \t]*\\(.*\\)[ \t]*$"))
2307 (not (assq (intern (match-string 1)) tags)))
2308 (error "Malformed line \"%s\""
2309 (buffer-substring (line-beginning-position)
2310 (line-end-position))))
2311 (push (cons (intern (match-string 1))
2312 (let ((val (match-string 2)))
2313 (if (string= "<keep>" val)
2314 t
2315 (set-text-properties 0 (length val) nil val)
2316 val)))
2317 tags)
2318 (forward-line)))
2319 (callf2 rassq-delete-all t tags)
2320 (with-temp-buffer
2321 (loop for (tag . value) in tags
2322 do (insert (symbol-name tag) "\n"
2323 value "\n"))
2324 (let ((input-buffer (current-buffer)))
2325 (ampc-with-buffer 'files-list
2326 no-se
2327 (let ((reporter
2328 (make-progress-reporter "Storing tags"
2329 0
2330 (let ((count (count-matches "^\\* ")))
2331 (if (zerop count)
2332 1
2333 count))))
2334 (step 0))
2335 (ampc-with-selection nil
2336 (let* ((data (get-text-property (point) 'data))
2337 (old-tags (loop for (tag . data) in (cdr data)
2338 collect (cons tag data)))
2339 (found-changed (ampc-tagger-tags-modified (cdr data) tags)))
2340 (let ((pre-hook-tags (cdr data)))
2341 (run-hook-with-args 'ampc-tagger-store-hook found-changed data)
2342 (setf found-changed
2343 (or found-changed
2344 (ampc-tagger-tags-modified pre-hook-tags
2345 (cdr data)))))
2346 (when found-changed
2347 (ampc-tagger-log
2348 "Storing tags for file "
2349 (abbreviate-file-name (car data)) "\n"
2350 "\tOld tags:\n"
2351 (loop for (tag . value) in old-tags
2352 concat (concat "\t\t"
2353 (symbol-name tag) ": "
2354 value "\n"))
2355 "\tNew tags:\n"
2356 (loop for (tag . value) in (cdr data)
2357 concat (concat "\t\t"
2358 (symbol-name tag) ": "
2359 value "\n")))
2360 (ampc-tagger-make-backup (car data))
2361 (ampc-tagger-report
2362 (list "--set" (car data))
2363 (with-temp-buffer
2364 (insert-buffer-substring input-buffer)
2365 (prog1
2366 (call-process-region (point-min) (point-max)
2367 ampc-tagger-executable
2368 nil t nil
2369 "--set" (car data))
2370 (when ampc-debug
2371 (message "ampc-tagger: %s"
2372 (buffer-substring
2373 (point-min) (point))))))))
2374 (run-hook-with-args 'ampc-tagger-stored-hook found-changed data)
2375 (let ((inhibit-read-only t))
2376 (move-beginning-of-line nil)
2377 (forward-char 2)
2378 (kill-line 1)
2379 (insert
2380 (ampc-pad (loop for p in (plist-get (cdr ampc-type)
2381 :properties)
2382 when (eq (car p) 'filename)
2383 collect (file-name-nondirectory (car data))
2384 else
2385 collect (cdr (assq (intern (car p))
2386 (cdr data)))
2387 end))
2388 "\n")
2389 (forward-line -1)
2390 (put-text-property (line-beginning-position)
2391 (1+ (line-end-position))
2392 'data data))
2393 (progress-reporter-update reporter (incf step))))
2394 (progress-reporter-done reporter)))))
2395 (when quit
2396 (ampc-tagger-quit (eq (prefix-numeric-value quit) 16))))
2397
2398 (defun ampc-tagger-quit (&optional no-update)
2399 "Quit tagger and restore previous window configuration.
2400 With optional prefix NO-UPDATE, do not trigger a database update."
2401 (interactive "P")
2402 (set-window-configuration (or (car-safe ampc-tagger-previous-configuration)
2403 ampc-tagger-previous-configuration))
2404 (when (car-safe ampc-tagger-previous-configuration)
2405 (unless no-update
2406 (ampc-trigger-update))
2407 (setf ampc-windows (cadr ampc-tagger-previous-configuration)))
2408 (setf ampc-tagger-previous-configuration nil))
2409
2410 (defun ampc-move-to-tab ()
2411 "Move point to next logical tab stop."
2412 (interactive)
2413 (let ((tab (loop for tab in
2414 (or (get-text-property (point) 'tab-stop-list) tab-stop-list)
2415 while (>= (current-column) tab)
2416 finally return tab)))
2417 (when tab
2418 (goto-char (min (+ (line-beginning-position) tab) (line-end-position))))))
2419
2420 (defun ampc-mouse-play-this (event)
2421 (interactive "e")
2422 (select-window (posn-window (event-end event)))
2423 (goto-char (posn-point (event-end event)))
2424 (ampc-play-this))
2425
2426 (defun ampc-mouse-delete (event)
2427 (interactive "e")
2428 (select-window (posn-window (event-end event)))
2429 (goto-char (posn-point (event-end event)))
2430 (ampc-delete 1))
2431
2432 (defun ampc-mouse-add (event)
2433 (interactive "e")
2434 (select-window (posn-window (event-end event)))
2435 (goto-char (posn-point (event-end event)))
2436 (ampc-add-impl))
2437
2438 (defun ampc-mouse-delete-playlist (event)
2439 (interactive "e")
2440 (select-window (posn-window (event-end event)))
2441 (goto-char (posn-point (event-end event)))
2442 (ampc-delete-playlist t))
2443
2444 (defun ampc-mouse-load (event)
2445 (interactive "e")
2446 (select-window (posn-window (event-end event)))
2447 (goto-char (posn-point (event-end event)))
2448 (ampc-load t))
2449
2450 (defun ampc-mouse-toggle-output-enabled (event)
2451 (interactive "e")
2452 (select-window (posn-window (event-end event)))
2453 (goto-char (posn-point (event-end event)))
2454 (ampc-toggle-output-enabled 1))
2455
2456 (defun* ampc-mouse-toggle-mark (event &aux (inhibit-read-only t))
2457 (interactive "e")
2458 (let ((window (posn-window (event-end event))))
2459 (when (with-selected-window window
2460 (goto-char (posn-point (event-end event)))
2461 (unless (eobp)
2462 (move-beginning-of-line nil)
2463 (ampc-mark-impl (not (eq (char-after) ?*)) 1)
2464 t))
2465 (select-window window))))
2466
2467 (defun ampc-mouse-align-point (event)
2468 (interactive "e")
2469 (select-window (posn-window (event-end event)))
2470 (goto-char (posn-point (event-end event)))
2471 (ampc-align-point))
2472
2473 (defun* ampc-unmark-all (&aux (inhibit-read-only t))
2474 "Remove all marks."
2475 (interactive)
2476 (assert (ampc-in-ampc-p t))
2477 (save-excursion
2478 (goto-char (point-min))
2479 (loop while (search-forward-regexp "^\\* " nil t)
2480 do (replace-match " " nil nil)))
2481 (ampc-post-mark-change-update))
2482
2483 (defun ampc-trigger-update ()
2484 "Trigger a database update."
2485 (interactive)
2486 (assert (ampc-on-p))
2487 (ampc-send-command 'update))
2488
2489 (defun* ampc-toggle-marks (&aux (inhibit-read-only t))
2490 "Toggle marks.
2491 Marked entries become unmarked, and vice versa."
2492 (interactive)
2493 (assert (ampc-in-ampc-p t))
2494 (save-excursion
2495 (loop for (a . b) in '(("* " . "T ")
2496 (" " . "* ")
2497 ("T " . " "))
2498 do (goto-char (point-min))
2499 (loop while (search-forward-regexp (concat "^" (regexp-quote a))
2500 nil
2501 t)
2502 do (replace-match b nil nil))))
2503 (ampc-post-mark-change-update))
2504
2505 (defun ampc-up (&optional arg)
2506 "Move selected entries ARG positions upwards.
2507 ARG defaults to one."
2508 (interactive "p")
2509 (assert (ampc-in-ampc-p))
2510 (ampc-move (- (or arg 1))))
2511
2512 (defun ampc-down (&optional arg)
2513 "Move selected entries ARG positions downwards.
2514 ARG defaults to one."
2515 (interactive "p")
2516 (assert (ampc-in-ampc-p))
2517 (ampc-move (or arg 1)))
2518
2519 (defun ampc-mark (&optional arg)
2520 "Mark the next ARG'th entries.
2521 ARG defaults to 1."
2522 (interactive "p")
2523 (assert (ampc-in-ampc-p t))
2524 (ampc-mark-impl t arg))
2525
2526 (defun ampc-unmark (&optional arg)
2527 "Unmark the next ARG'th entries.
2528 ARG defaults to 1."
2529 (interactive "p")
2530 (assert (ampc-in-ampc-p t))
2531 (ampc-mark-impl nil arg))
2532
2533 (defun ampc-set-volume (&optional arg)
2534 "Set volume to ARG percent.
2535 If ARG is nil, read ARG from minibuffer."
2536 (interactive "P")
2537 (assert (ampc-on-p))
2538 (ampc-set-volume-impl (or arg (read-number "Volume: "))))
2539
2540 (defun ampc-increase-volume (&optional arg)
2541 "Increase volume by prefix argument ARG or, if ARG is nil,
2542 `ampc-volume-step'."
2543 (interactive "P")
2544 (assert (ampc-on-p))
2545 (ampc-set-volume-impl arg '+))
2546
2547 (defun ampc-decrease-volume (&optional arg)
2548 "Decrease volume by prefix argument ARG or, if ARG is nil,
2549 `ampc-volume-step'."
2550 (interactive "P")
2551 (assert (ampc-on-p))
2552 (ampc-set-volume-impl arg '-))
2553
2554 (defun ampc-set-crossfade (&optional arg)
2555 "Set crossfade to ARG seconds.
2556 If ARG is nil, read ARG from minibuffer."
2557 (interactive "P")
2558 (assert (ampc-on-p))
2559 (ampc-set-crossfade-impl (or arg (read-number "Crossfade: "))))
2560
2561 (defun ampc-increase-crossfade (&optional arg)
2562 "Increase crossfade by prefix argument ARG or, if ARG is nil,
2563 `ampc-crossfade-step'."
2564 (interactive "P")
2565 (assert (ampc-on-p))
2566 (ampc-set-crossfade-impl arg '+))
2567
2568 (defun ampc-decrease-crossfade (&optional arg)
2569 "Decrease crossfade by prefix argument ARG or, if ARG is nil,
2570 `ampc-crossfade-step'."
2571 (interactive "P")
2572 (assert (ampc-on-p))
2573 (ampc-set-crossfade-impl arg '-))
2574
2575 (defun ampc-toggle-repeat (&optional arg)
2576 "Toggle MPD's repeat state.
2577 With prefix argument ARG, enable repeating if ARG is positive,
2578 otherwise disable it."
2579 (interactive "P")
2580 (assert (ampc-on-p))
2581 (ampc-toggle-state 'repeat arg))
2582
2583 (defun ampc-toggle-consume (&optional arg)
2584 "Toggle MPD's consume state.
2585 With prefix argument ARG, enable consuming if ARG is positive,
2586 otherwise disable it.
2587
2588 When consume is activated, each song played is removed from the playlist."
2589 (interactive "P")
2590 (assert (ampc-on-p))
2591 (ampc-toggle-state 'consume arg))
2592
2593 (defun ampc-toggle-random (&optional arg)
2594 "Toggle MPD's random state.
2595 With prefix argument ARG, enable random playing if ARG is positive,
2596 otherwise disable it."
2597 (interactive "P")
2598 (ampc-toggle-state 'random arg))
2599
2600 (defun ampc-play-this (&optional arg)
2601 "Play selected song.
2602 With prefix argument ARG, play the ARG'th song located at the
2603 zero-indexed position of the current playlist."
2604 (interactive "P")
2605 (assert (and (ampc-on-p) (or arg (ampc-in-ampc-p))))
2606 (if (not arg)
2607 (unless (eobp)
2608 (ampc-send-command 'play nil (1- (line-number-at-pos)))
2609 (ampc-send-command 'pause nil 0))
2610 (ampc-send-command 'play nil arg)
2611 (ampc-send-command 'pause nil 0)))
2612
2613 (defun* ampc-toggle-play
2614 (&optional arg &aux (state (cdr (assq 'state ampc-status))))
2615 "Toggle play state.
2616 If MPD does not play a song already, start playing the song at
2617 point if the current buffer is the playlist buffer, otherwise
2618 start at the beginning of the playlist.
2619
2620 If ARG is 4, stop player rather than pause if applicable."
2621 (interactive "P")
2622 (assert (ampc-on-p))
2623 (unless state
2624 (return-from ampc-toggle-play))
2625 (when arg
2626 (setf arg (prefix-numeric-value arg)))
2627 (ecase (intern state)
2628 (stop
2629 (when (or (null arg) (> arg 0))
2630 (ampc-send-command
2631 'play
2632 '(:remove-other (pause))
2633 (if (and (eq (car ampc-type) 'current-playlist) (not (eobp)))
2634 (1- (line-number-at-pos))
2635 0))))
2636 (pause
2637 (when (or (null arg) (> arg 0))
2638 (ampc-send-command 'pause '(:remove-other (play)) 0)))
2639 (play
2640 (cond ((or (null arg) (< arg 0))
2641 (ampc-send-command 'pause '(:remove-other (play)) 1))
2642 ((eq arg 4)
2643 (ampc-send-command 'stop))))))
2644
2645 (defun ampc-next (&optional arg)
2646 "Play next song.
2647 With prefix argument ARG, skip ARG songs."
2648 (interactive "p")
2649 (assert (ampc-on-p))
2650 (ampc-skip (or arg 1)))
2651
2652 (defun ampc-previous (&optional arg)
2653 "Play previous song.
2654 With prefix argument ARG, skip ARG songs."
2655 (interactive "p")
2656 (assert (ampc-on-p))
2657 (ampc-skip (- (or arg 1))))
2658
2659 (defun ampc-rename-playlist (new-name)
2660 "Rename selected playlist to NEW-NAME.
2661 If NEW-NAME is nil, read NEW-NAME from the minibuffer."
2662 (interactive "M")
2663 (unless new-name
2664 (setf new-name (read-from-minibuffer (concat "New name for playlist "
2665 (ampc-playlist)
2666 ": "))))
2667 (assert (ampc-in-ampc-p))
2668 (if (ampc-playlist)
2669 (ampc-send-command 'rename '(:full-remove t) (ampc-quote new-name))
2670 (message "No playlist selected")))
2671
2672 (defun ampc-load (&optional at-point)
2673 "Load selected playlist in the current playlist.
2674 If optional argument AT-POINT is non-nil (or if no playlist is
2675 selected), use playlist at point rather than the selected one."
2676 (interactive)
2677 (assert (ampc-in-ampc-p))
2678 (if (ampc-playlist at-point)
2679 (ampc-send-command
2680 'load '(:keep-prev t)
2681 (ampc-quote (ampc-playlist at-point)))
2682 (if at-point
2683 (message "No playlist at point")
2684 (message "No playlist selected"))))
2685
2686 (defun ampc-toggle-output-enabled (&optional arg)
2687 "Toggle the next ARG outputs.
2688 If ARG is omitted, use the selected entries."
2689 (interactive "P")
2690 (assert (ampc-in-ampc-p))
2691 (ampc-with-selection arg
2692 (let ((data (get-text-property (point) 'data)))
2693 (ampc-send-command (if (equal (cdr (assoc "outputenabled" data)) "1")
2694 'disableoutput
2695 'enableoutput)
2696 '(:full-remove t)
2697 (cdr (assoc "outputid" data))))))
2698
2699 (defun ampc-delete (&optional arg)
2700 "Delete the next ARG songs from the playlist.
2701 If ARG is omitted, use the selected entries. If ARG is non-nil,
2702 all marks after point are removed nontheless."
2703 (interactive "P")
2704 (assert (ampc-in-ampc-p))
2705 (let ((first-del nil))
2706 (ampc-with-selection arg
2707 (unless (or first-del (when arg (< arg 0)))
2708 (setf first-del (point)))
2709 (let ((val (1- (- (line-number-at-pos) (if (or (not arg) (> arg 0))
2710 index
2711 0)))))
2712 (if (and (not (eq (car ampc-type) 'current-playlist)) (ampc-playlist))
2713 (ampc-send-command 'playlistdelete
2714 '(:keep-prev t)
2715 (ampc-quote (ampc-playlist))
2716 val)
2717 (ampc-send-command 'delete '(:keep-prev t) val))
2718 (ampc-mark-impl nil nil)))
2719 (when first-del
2720 (goto-char first-del))))
2721
2722 (defun ampc-shuffle ()
2723 "Shuffle playlist."
2724 (interactive)
2725 (assert (ampc-on-p))
2726 (if (and (not (eq (car ampc-type) 'current-playlist)) (ampc-playlist))
2727 (ampc-send-command 'shuffle-listplaylistinfo
2728 `(:playlist ,(ampc-playlist))
2729 (ampc-quote (ampc-playlist)))
2730 (ampc-send-command 'shuffle)))
2731
2732 (defun ampc-clear ()
2733 "Clear playlist."
2734 (interactive)
2735 (assert (ampc-on-p))
2736 (if (and (not (eq (car ampc-type) 'current-playlist)) (ampc-playlist))
2737 (ampc-send-command 'playlistclear '(:full-remove t)
2738 (ampc-quote (ampc-playlist)))
2739 (ampc-send-command 'clear)))
2740
2741 (defun ampc-add (&optional arg)
2742 "Add the songs associated with the next ARG entries after point
2743 to the playlist.
2744 If ARG is omitted, use the selected entries in the current buffer."
2745 (interactive "P")
2746 (assert (ampc-in-ampc-p))
2747 (ampc-with-selection arg
2748 (ampc-add-impl)))
2749
2750 (defun ampc-status (&optional no-print)
2751 "Display and return the information that is displayed in the status window.
2752 If optional argument NO-PRINT is non-nil, just return the text.
2753 If NO-PRINT is nil, the display may be delayed if ampc does not
2754 have enough information yet."
2755 (interactive)
2756 (assert (ampc-on-p))
2757 (unless (or ampc-status no-print)
2758 (ampc-send-command 'status)
2759 (ampc-send-command 'mini-currentsong)
2760 (return-from ampc-status))
2761 (let* ((flags (mapconcat
2762 'identity
2763 (loop for (f . n) in '((repeat . "Repeat")
2764 (random . "Random")
2765 (consume . "Consume"))
2766 when (equal (cdr (assq f ampc-status)) "1")
2767 collect n
2768 end)
2769 "|"))
2770 (state (cdr (assq 'state ampc-status)))
2771 (status (concat "State: " state
2772 (when (and ampc-yield no-print)
2773 (concat (make-string (- 10 (length state)) ? )
2774 (nth (% ampc-yield 4) '("|" "/" "-" "\\"))))
2775 "\n"
2776 (when (equal state "play")
2777 (concat "Playing: "
2778 (ampc-clean-tag
2779 'Artist
2780 (cdr (assq 'Artist ampc-status)))
2781 " - "
2782 (ampc-clean-tag
2783 'Title
2784 (cdr (assq 'Title ampc-status)))
2785 "\n"))
2786 "Volume: " (cdr (assq 'volume ampc-status)) "\n"
2787 "Crossfade: " (cdr (assq 'xfade ampc-status))
2788 (unless (equal flags "")
2789 (concat "\n" flags)))))
2790 (unless no-print
2791 (message "%s" status))
2792 status))
2793
2794 (defun ampc-delete-playlist (&optional at-point)
2795 "Delete selected playlist.
2796 If optional argument AT-POINT is non-nil (or if no playlist is
2797 selected), use playlist at point rather than the selected one."
2798 (interactive)
2799 (assert (ampc-in-ampc-p))
2800 (if (ampc-playlist at-point)
2801 (when (y-or-n-p (concat "Delete playlist " (ampc-playlist at-point) "?"))
2802 (ampc-send-command 'rm '(:full-remove t)
2803 (ampc-quote (ampc-playlist at-point))))
2804 (if at-point
2805 (message "No playlist at point")
2806 (message "No playlist selected"))))
2807
2808 ;;;###autoload
2809 (defun ampc-tagger-dired (&optional arg)
2810 "Start the tagging subsystem on dired's marked files.
2811 With optional prefix argument ARG, use the next ARG files."
2812 (interactive "P")
2813 (assert (derived-mode-p 'dired-mode))
2814 (ampc-tag-files
2815 (loop for file in (dired-map-over-marks (dired-get-filename) arg)
2816 unless (file-directory-p file)
2817 collect file
2818 end)))
2819
2820 ;;;###autoload
2821 (defun ampc-tag-files (files)
2822 "Start the tagging subsystem.
2823 FILES should be a list of absolute file names, the files to tag."
2824 (unless files
2825 (message "No files specified")
2826 (return-from ampc-tagger-files t))
2827 (when (memq (car ampc-type) '(files-list tagger))
2828 (message "You are already within the tagger")
2829 (return-from ampc-tagger-files t))
2830 (let ((reporter (make-progress-reporter "Grabbing tags" 0 (length files))))
2831 (loop for file in-ref files
2832 for i from 1
2833 do (run-hook-with-args 'ampc-tagger-grab-hook file)
2834 (with-temp-buffer
2835 (ampc-tagger-call "--get" file)
2836 (setf file
2837 (apply 'list
2838 file
2839 (loop for tag in ampc-tagger-tags
2840 collect
2841 (cons tag (or (ampc-extract (ampc-extract-regexp
2842 (symbol-name tag)))
2843 ""))))))
2844 (run-hook-with-args 'ampc-tagger-grabbed-hook file)
2845 (progress-reporter-update reporter i))
2846 (progress-reporter-done reporter))
2847 (unless ampc-tagger-previous-configuration
2848 (setf ampc-tagger-previous-configuration (current-window-configuration)))
2849 (ampc-configure-frame (cdr (assq 'tagger ampc-views)) t)
2850 (ampc-with-buffer 'files-list
2851 (erase-buffer)
2852 (loop for (file . props) in files
2853 do (insert (propertize
2854 (concat
2855 " "
2856 (ampc-pad
2857 (loop for p in (plist-get (cdr ampc-type) :properties)
2858 when (eq (car p) 'filename)
2859 collect (file-name-nondirectory file)
2860 else
2861 collect (cdr (assq (intern (car p)) props))
2862 end))
2863 "\n")
2864 'data (cons file props))))
2865 (ampc-set-dirty nil)
2866 (ampc-toggle-marks))
2867 (ampc-with-buffer 'tagger
2868 no-se
2869 (ampc-tagger-reset t)
2870 (goto-char (point-min))
2871 (search-forward-regexp ": *")
2872 (ampc-set-dirty nil))
2873 nil)
2874
2875 (defun* ampc-tagger (&optional arg &aux files)
2876 "Start the tagging subsystem.
2877 The files to tag are collected by using either the selected
2878 entries within the current buffer or the next ARG entries at
2879 point if numeric perfix argument ARG is non-nil, the file
2880 associated with the entry at point, or, if both sources did not
2881 provide any files, the audio file that is currently played by
2882 MPD."
2883 (interactive "P")
2884 (assert (ampc-in-ampc-p))
2885 (unless ampc-tagger-version-verified
2886 (with-temp-buffer
2887 (ampc-tagger-call "--version")
2888 (goto-char (point-min))
2889 (let ((version (buffer-substring (line-beginning-position)
2890 (line-end-position))))
2891 (unless (equal version ampc-tagger-version)
2892 (message (concat "The reported version of %s is not supported - "
2893 "got \"%s\", want \"%s\"")
2894 ampc-tagger-executable
2895 version
2896 ampc-tagger-version)
2897 (return-from ampc-tagger))))
2898 (setf ampc-tagger-version-verified t))
2899 (unless ampc-tagger-genres
2900 (with-temp-buffer
2901 (ampc-tagger-call "--genres")
2902 (loop while (search-backward-regexp "^\\(.+\\)$" nil t)
2903 do (push (match-string 1) ampc-tagger-genres))))
2904 (unless ampc-tagger-music-directories
2905 (message (concat "ampc-tagger-music-directories is nil. Fill it via "
2906 "M-x customize-variable RET ampc-tagger-music-directories "
2907 "RET"))
2908 (return-from ampc-tagger))
2909 (case (car ampc-type)
2910 (current-playlist
2911 (save-excursion
2912 (ampc-with-selection arg
2913 (callf nconc files (list (cdr (assoc "file" (get-text-property
2914 (line-end-position)
2915 'data))))))))
2916 ((playlist tag song)
2917 (save-excursion
2918 (ampc-with-selection arg
2919 (ampc-on-files (lambda (file) (push file files)))))
2920 (callf nreverse files))
2921 (t
2922 (let ((file (cdr (assoc 'file ampc-status))))
2923 (when file
2924 (setf files (list file))))))
2925 (loop for file in-ref files
2926 for read-file = (locate-file file ampc-tagger-music-directories)
2927 do (unless read-file
2928 (error "Cannot locate file %s in ampc-tagger-music-directories"
2929 file)
2930 (return-from ampc-tagger))
2931 (setf file (expand-file-name read-file)))
2932 (setf ampc-tagger-previous-configuration
2933 (list (current-window-configuration) ampc-windows))
2934 (when (ampc-tag-files files)
2935 (setf ampc-tagger-previous-configuration nil)))
2936
2937 (defun ampc-store (&optional name-or-append)
2938 "Store current playlist as NAME-OR-APPEND.
2939 If NAME is non-nil and not a string, append selected entries
2940 within the current playlist buffer to the selected playlist. If
2941 NAME-OR-APPEND is a negative integer, append the next (-
2942 NAME-OR-APPEND) entries after point within the current playlist
2943 buffer to the selected playlist. If NAME-OR-APPEND is nil, read
2944 playlist name from the minibuffer."
2945 (interactive "P")
2946 (assert (ampc-in-ampc-p))
2947 (unless name-or-append
2948 (setf name-or-append (read-from-minibuffer "Save playlist as: ")))
2949 (if (stringp name-or-append)
2950 (ampc-send-command 'save '(:full-remove t) (ampc-quote name-or-append))
2951 (if (not (ampc-playlist))
2952 (message "No playlist selected")
2953 (ampc-with-buffer 'current-playlist
2954 (when name-or-append
2955 (callf prefix-numeric-value name-or-append))
2956 (ampc-with-selection (if (and name-or-append (< name-or-append 0))
2957 (- name-or-append)
2958 nil)
2959 (ampc-send-command
2960 'playlistadd
2961 '(:keep-prev t)
2962 (ampc-quote (ampc-playlist))
2963 (ampc-quote (cdr (assoc "file"
2964 (get-text-property (point) 'data))))))))))
2965
2966 (defun* ampc-goto-current-song (&aux (song (cdr (assq 'song ampc-status))))
2967 "Select the current playlist window and move point to the current song."
2968 (interactive)
2969 (assert (ampc-in-ampc-p))
2970 (let ((window (ampc-with-buffer 'current-playlist
2971 (selected-window))))
2972 (when window
2973 (select-window window)
2974 (when song
2975 (goto-char (point-min))
2976 (forward-line (string-to-number song)))
2977 (ampc-align-point))))
2978
2979 (defun ampc-previous-line (&optional arg)
2980 "Go to previous ARG'th entry in the current buffer.
2981 ARG defaults to 1."
2982 (interactive "p")
2983 (assert (ampc-in-ampc-p t))
2984 (ampc-next-line (* (or arg 1) -1)))
2985
2986 (defun ampc-next-line (&optional arg)
2987 "Go to next ARG'th entry in the current buffer.
2988 ARG defaults to 1."
2989 (interactive "p")
2990 (assert (ampc-in-ampc-p t))
2991 (forward-line arg)
2992 (if (eobp)
2993 (progn (forward-line -1)
2994 (forward-char 2)
2995 t)
2996 (ampc-align-point)
2997 nil))
2998
2999 (defun* ampc-suspend (&optional (run-hook t))
3000 "Suspend ampc.
3001 This function resets the window configuration, but does not close
3002 the connection to MPD or destroy the internal cache of ampc.
3003 This means subsequent startups of ampc will be faster."
3004 (interactive)
3005 (when ampc-working-timer
3006 (cancel-timer ampc-working-timer))
3007 (ampc-restore-window-configuration)
3008 (loop for b in ampc-all-buffers
3009 do (when (buffer-live-p b)
3010 (kill-buffer b)))
3011 (setf ampc-windows nil
3012 ampc-all-buffers nil
3013 ampc-working-timer nil)
3014 (when run-hook
3015 (run-hooks 'ampc-suspend-hook)))
3016
3017 (defun ampc-mini ()
3018 "Select song to play via `completing-read'."
3019 (interactive)
3020 (assert (ampc-on-p))
3021 (ampc-send-command 'mini-playlistinfo))
3022
3023 (defun ampc-quit (&optional arg)
3024 "Quit ampc.
3025 If prefix argument ARG is non-nil, kill the MPD instance that
3026 ampc is connected to."
3027 (interactive "P")
3028 (when (ampc-on-p)
3029 (set-process-filter ampc-connection nil)
3030 (when (equal (car-safe ampc-outstanding-commands) '(idle nil))
3031 (ampc-send-command-impl "noidle")
3032 (with-current-buffer (process-buffer ampc-connection)
3033 (loop do (goto-char (point-min))
3034 until (search-forward-regexp "^\\(ACK\\)\\|\\(OK\\).*\n\\'" nil t)
3035 while (ampc-on-p)
3036 do (accept-process-output ampc-connection nil 50))))
3037 (ampc-send-command-impl (if arg "kill" "close"))
3038 (delete-process ampc-connection))
3039 (when ampc-working-timer
3040 (cancel-timer ampc-working-timer))
3041 (ampc-suspend nil)
3042 (setf ampc-connection nil
3043 ampc-internal-db nil
3044 ampc-outstanding-commands nil
3045 ampc-status nil)
3046 (run-hooks 'ampc-quit-hook))
3047
3048 ;;;###autoload
3049 (defun ampc-suspended-p ()
3050 "Return non-nil if ampc is suspended."
3051 (interactive)
3052 (and (ampc-on-p) (not ampc-windows)))
3053
3054 ;;;###autoload
3055 (defun ampc-on-p ()
3056 "Return non-nil if ampc is connected to the daemon."
3057 (interactive)
3058 (and ampc-connection (memq (process-status ampc-connection) '(open run))))
3059
3060 ;;;###autoload
3061 (defun ampc (&optional host port suspend)
3062 "ampc is an asynchronous client for the MPD media player.
3063 This function is the main entry point for ampc.
3064
3065 HOST and PORT specify the MPD instance to connect to. The values
3066 default to the ones specified in `ampc-default-server'."
3067 (interactive)
3068 (unless (byte-code-function-p (symbol-function 'ampc))
3069 (message "You should byte-compile ampc"))
3070 (run-hooks 'ampc-before-startup-hook)
3071 (unless host
3072 (setf host (or (car ampc-default-server) (read-string "Host: "))))
3073 (unless port
3074 (setf port (or (cdr ampc-default-server) (read-string "Port: "))))
3075 (when (and ampc-connection
3076 (or (not (equal host ampc-host))
3077 (not (equal port ampc-port))
3078 (not (ampc-on-p))))
3079 (ampc-quit))
3080 (unless ampc-connection
3081 (let ((connection (open-network-stream "ampc"
3082 (with-current-buffer
3083 (get-buffer-create " *ampc*")
3084 (erase-buffer)
3085 (current-buffer))
3086 host
3087 port
3088 :type 'plain :return-list t)))
3089 (unless (car connection)
3090 (error "Failed connecting to server: %s"
3091 (plist-get ampc-connection :error)))
3092 (setf ampc-connection (car connection)
3093 ampc-host host
3094 ampc-port port))
3095 (set-process-coding-system ampc-connection 'utf-8-unix 'utf-8-unix)
3096 (set-process-filter ampc-connection 'ampc-filter)
3097 (set-process-query-on-exit-flag ampc-connection nil)
3098 (setf ampc-outstanding-commands '((setup))))
3099 (if suspend
3100 (ampc-update)
3101 (ampc-configure-frame (cddadr ampc-views)))
3102 (run-hooks 'ampc-connected-hook)
3103 (when suspend
3104 (ampc-suspend))
3105 (ampc-filter (process-buffer ampc-connection) nil))
3106
3107 (provide 'ampc)
3108
3109 ;; Local Variables:
3110 ;; eval: (outline-minor-mode 1)
3111 ;; outline-regexp: ";;; \\*+"
3112 ;; fill-column: 80
3113 ;; indent-tabs-mode: nil
3114 ;; End: