;; Author: Christopher Schmidt <christopher@ch.ristopher.com>
;; Maintainer: Christopher Schmidt <christopher@ch.ristopher.com>
-;; Version: 0.1.2
+;; Version: 0.1.3
;; Created: 2011-12-06
;; Keywords: ampc, mpc, mpd
;; Compatibility: GNU Emacs: 24.x
;; your load-path or add the directory the file is in to it, e.g.:
;;
;; (add-to-list 'load-path "~/.emacs.d/ampc")
-;;
-;; Then add one autoload definition:
-;;
;; (autoload 'ampc "ampc" nil t)
;;
-;; Optionally bind a key to this function, e.g.:
-;;
-;; (global-set-key (kbd "<f9>") 'ampc)
-;;
-;; or
-;;
-;; (global-set-key (kbd "<f9>") (lambda () (interactive) (ampc "host" "port")))
-;;
;; Byte-compile ampc (M-x byte-compile-file RET /path/to/ampc.el RET) to improve
;; its performance!
;; interactively, `ampc' reads host address and port from the minibuffer. If
;; called non-interactively, the first argument to `ampc' is the host, the
;; second is the port. Both values default to nil, which will make ampc connect
-;; to localhost:6600. Once ampc is connected to the daemon, it creates its
-;; window configuration in the selected window. To make ampc use the full frame
-;; rather than the selected window, customise `ampc-use-full-frame'.
+;; to localhost:6600. If the optional third argument is non-nil and ampc is
+;; connected to the daemon, it creates its window configuration in the selected
+;; window. To make ampc use the full frame rather than the selected window,
+;; customise `ampc-use-full-frame'. To check whether ampc is connected to the
+;; daemon, call `ampc-is-on-p' and `ampc-suspended-p'.
;;
;; ampc offers three independent views which expose different parts of the user
;; interface. The current playlist view, the default view at startup, may be
;;
;; To mark an entry, move the point to the entry and press `m' (ampc-mark). To
;; unmark an entry, press `u' (ampc-unmark). To unmark all entries, press `U'
-;; (ampc-unmark-all). To toggle marks, press `t' (ampc-toggle-marks). To
-;; navigate to the next entry, press `n' (ampc-next-line). Analogous, pressing
-;; `p' (ampc-previous-line) moves the point to the previous entry.
+;; (ampc-unmark-all). To toggle marks, press `t' (ampc-toggle-marks). Pressing
+;; `<down-mouse-1>' with the mouse mouse cursor on a list entry will move point
+;; to the entry and toggle the mark. To navigate to the next entry, press `n'
+;; (ampc-next-line). Analogous, pressing `p' (ampc-previous-line) moves the
+;; point to the previous entry.
;;
;; Window two shows the current playlist. The song that is currently played by
;; the daemon, if any, is highlighted. To delete the selected songs from the
-;; playlist, press `d' (ampc-delete). To move the selected songs up, press
-;; `<up>' (ampc-up). Analogous, press `<down>' (ampc-down) to move the selected
-;; songs down.
+;; playlist, press `d' (ampc-delete). Pressing `<down-mouse-3>' will move the
+;; point to the entry under cursor and delete it from the playlist. To move the
+;; selected songs up, press `<up>' (ampc-up). Analogous, press `<down>'
+;; (ampc-down) to move the selected songs down. Pressing `<return>'
+;; (ampc-play-this) or `<down-mouse-2>' will play the song at point/cursor.
;;
;; Windows three to five are tag browsers. You use them to narrow the song
;; database to certain songs. Think of tag browsers as filters, analogous to
;; songs that is filtered is displayed in the header line of the window.
;;
;; Window six shows the songs that match the filters defined by windows three to
-;; five. To add the selected song to the playlist, press `a' (ampc-add). This
-;; key binding works in tag browsers as well. Calling `ampc-add' in a tag
-;; browser adds all songs filtered up to the selected browser to the playlist.
+;; five. To add the selected song to the playlist, press `a' (ampc-add).
+;; Pressing `<down-mouse-3>' will move the point to the entry under the cursor
+;; and execute `ampc-add'. These key bindings works in tag browsers as well.
+;; Calling `ampc-add' in a tag browser adds all songs filtered up to the
+;; selected browser to the playlist.
;;
;; The tag browsers of the (default) current playlist view (accessed via `J')
;; are `Genre' (window 3), `Artist' (window 4) and `Album' (window 5). The key
;; stored playlists is the only view in ampc that may have only one marked
;; entry.
;;
+;; To queue a playlist, press `l' (ampc-load) or `<down-mouse-2>'. To delete a
+;; playlist, press `d' (ampc-delete-playlist) or `<down-mouse-3>'. The command
+;; `ampc-rename-playlist', bound to `r', can be used to rename a playlist.
+;;
;; Again, the key `<' may be used to setup a playlist view with a different
;; order of tag browsers.
;;; *** outputs view
;; The outputs view contains a single list which shows the configured outputs of
-;; mpd. To toggle the enabled property of the selected outputs, press `a'
-;; (ampc-toggle-output-enabled).
+;; MPD. To toggle the enabled property of the selected outputs, press `a'
+;; (ampc-toggle-output-enabled) or `<mouse-3>'.
;;; *** global keys
;; Aside from `J', `M', `K', `<' and `L', which may be used to select different
;; views, ampc defines the following global keys, which may be used in every
;; window associated with ampc:
;;
-;; `k' (ampc-toggle-play): Toggle play state. If mpd does not play a song
+;; `k' (ampc-toggle-play): Toggle play state. If MPD does not play a song
;; already, start playing the song at point if the current buffer is the
;; playlist buffer, otherwise start at the beginning of the playlist. With
;; prefix argument 4, stop player rather than pause if applicable.
;;
;; `y' (ampc-increase-volume): Increase volume.
;; `M-y' (ampc-decrease-volume): Decrease volume.
+;; `C-M-y' (ampc-set-volume): Set volume.
;; `h' (ampc-increase-crossfade): Increase crossfade.
;; `M-h' (ampc-decrease-crossfade): Decrease crossfade.
;;
;;
;; `P' (ampc-goto-current-song): Select the current playlist window and move
;; point to the current song.
+;; `G' (ampc-mini): Select song to play via `completing-read'.
;;
;; `T' (ampc-trigger-update): Trigger a database update.
;; `Z' (ampc-suspend): Suspend ampc.
;; (substitute-ampc-key (kbd "z") (kbd "Z"))
;; (substitute-ampc-key (kbd "y") (kbd "z"))
;; (substitute-ampc-key (kbd "M-y") (kbd "M-z"))
+;; (substitute-ampc-key (kbd "C-M-y") (kbd "C-M-z"))
;; (substitute-ampc-key (kbd "<") (kbd ";"))))
;;
;; If ampc is suspended, you can still use every interactive command that does
;; not directly operate on or with the user interace of ampc. For example it is
;; perfectly fine to call `ampc-increase-volume' or `ampc-toggle-play' via M-x
-;; RET. To display the information that is displayed by the status window of
-;; ampc, call `ampc-status'.
+;; RET. Especially the commands `ampc-status' and `ampc-mini' are predestinated
+;; to be bound in the global keymap. `ampc-status' messages the information
+;; that is displayed by the status window of ampc. `ampc-mini' lets you select
+;; a song to play via `completing-read'.
+;;
+;; (global-set-key (kbd "<f7>")
+;; (lambda ()
+;; (interactive)
+;; (unless (ampc-on-p)
+;; (ampc nil nil t))
+;; (ampc-status)))
+;; (global-set-key (kbd "<f8>")
+;; (lambda ()
+;; (interactive)
+;; (unless (ampc-on-p)
+;; (ampc nil nil t))
+;; (ampc-mini)))
;;; Code:
;;; * code
(require 'avl-tree)
;;; ** declarations
-;;; *** variables
(defgroup ampc ()
"Asynchronous client for the Music Player Daemon."
:prefix "ampc-"
"If non-nil, truncate lines in ampc buffers."
:type 'boolean)
+(defcustom ampc-default-server '("localhost" . 6600)
+ "The MPD server to connect to if the arguments to `ampc' are nil.
+This variable is a cons cell, with the car specifying the
+hostname and the cdr specifiying the port. Both values can be
+nil, which will make ampc query the user for values on each
+invocation."
+ :type '(cons (choice :tag "Hostname"
+ (string)
+ (const :tag "Ask" nil))
+ (choice :tag "Port"
+ (string)
+ (integer)
+ (const :tag "Ask" nil))))
+
+(defcustom ampc-synchronous-commands '(t status currentsong)
+ "List of MPD commands that should be executed synchronously.
+Executing commands that print lots of output synchronously will
+result in massive performance improvements of ampc. If the car
+of this list is `t', execute all commands synchronously other
+than the ones specified by the rest of the list."
+ :type '(repeat symbol))
+
(defcustom ampc-status-tags nil
"List of additional tags of the current song that are added to
the internal status of ampc and thus are passed to the functions
in `ampc-status-changed-hook'. Each element may be a string that
specifies a tag that is returned by MPD's `currentsong'
-command.")
+command."
+ :type '(list symbol))
+
+(defcustom ampc-volume-step 5
+ "Default step of `ampc-increase-volume' and
+`ampc-decrease-volume' to change the volume."
+ :type 'integer)
;;; **** hooks
(defcustom ampc-before-startup-hook nil
;;; *** internal variables
(defvar ampc-views
- (let* ((songs '(1.0 song :properties (("Track" :title "#")
- ("Title" :offset 6)
- ("Time" :offset 26))))
+ (let* ((songs '(1.0 song :properties (("Track" :title "#" :width 4)
+ ("Title" :min 15 :max 40)
+ ("Time" :width 6))))
(rs_a `(1.0 vertical
(0.7 horizontal
(0.33 tag :tag "Genre" :id 1)
(0.33 tag :tag "Album" :id 2)
(1.0 tag :tag "Artist" :id 3))
,songs))
- (pl-prop '(("Title")
- ("Artist" :offset 20)
- ("Album" :offset 40)
- ("Time" :offset 60))))
+ (pl-prop '(:properties (("Title" :min 15 :max 40)
+ ("Artist" :min 15 :max 40)
+ ("Album" :min 15 :max 40)
+ ("Time" :width 6)))))
`(("Current playlist view (Genre|Artist|Album)"
,(kbd "J")
horizontal
(0.4 vertical
(6 status)
- (1.0 current-playlist :properties ,pl-prop))
+ (1.0 current-playlist ,@pl-prop))
,rs_a)
("Current playlist view (Genre|Album|Artist)"
,(kbd "M")
horizontal
(0.4 vertical
(6 status)
- (1.0 current-playlist :properties ,pl-prop))
+ (1.0 current-playlist ,@pl-prop))
,rs_b)
("Playlist view (Genre|Artist|Album)"
,(kbd "K")
(0.4 vertical
(6 status)
(1.0 vertical
- (0.8 playlist :properties ,pl-prop)
+ (0.8 playlist ,@pl-prop)
(1.0 playlists)))
,rs_a)
("Playlist view (Genre|Album|Artist)"
(0.4 vertical
(6 status)
(1.0 vertical
- (0.8 playlist :properties ,pl-prop)
+ (0.8 playlist ,@pl-prop)
(1.0 playlists)))
,rs_b)
("Outputs view"
,(kbd "L")
- outputs :properties (("outputname" :title "Name")
- ("outputenabled" :title "Enabled" :offset 10))))))
+ outputs :properties (("outputname" :title "Name" :min 10 :max 30)
+ ("outputenabled" :title "Enabled" :width 9))))))
(defvar ampc-connection nil)
(defvar ampc-host nil)
(defvar ampc-port nil)
(defvar ampc-outstanding-commands nil)
+(defvar ampc-no-implicit-next-dispatch nil)
(defvar ampc-working-timer nil)
(defvar ampc-yield nil)
(defvar ampc-buffers-unordered nil)
(defvar ampc-all-buffers nil)
+(defvar ampc-tab-offsets nil)
+(make-variable-buffer-local 'ampc-tab-offsets)
+
(defvar ampc-type nil)
(make-variable-buffer-local 'ampc-type)
(defvar ampc-dirty nil)
(define-key map (kbd "D") 'ampc-delete-playlist)
(define-key map (kbd "y") 'ampc-increase-volume)
(define-key map (kbd "M-y") 'ampc-decrease-volume)
+ (define-key map (kbd "C-M-y") 'ampc-set-volume)
(define-key map (kbd "h") 'ampc-increase-crossfade)
(define-key map (kbd "M-h") 'ampc-decrease-crossfade)
(define-key map (kbd "e") 'ampc-toggle-repeat)
(define-key map (kbd "r") 'ampc-toggle-random)
(define-key map (kbd "f") 'ampc-toggle-consume)
(define-key map (kbd "P") 'ampc-goto-current-song)
+ (define-key map (kbd "G") 'ampc-mini)
(define-key map (kbd "q") 'ampc-quit)
(define-key map (kbd "z") 'ampc-suspend)
(define-key map (kbd "T") 'ampc-trigger-update)
(define-key map (kbd "p") 'ampc-previous-line)
(define-key map [remap next-line] 'ampc-next-line)
(define-key map [remap previous-line] 'ampc-previous-line)
+ (define-key map (kbd "<down-mouse-1>") 'ampc-mouse-toggle-mark)
+ (define-key map (kbd "<mouse-1>") 'ampc-mouse-align-point)
map))
(defvar ampc-current-playlist-mode-map
(let ((map (make-sparse-keymap)))
(suppress-keymap map)
(define-key map (kbd "<return>") 'ampc-play-this)
+ (define-key map (kbd "<down-mouse-2>") 'ampc-mouse-play-this)
+ (define-key map (kbd "<mouse-2>") 'ampc-mouse-align-point)
+ (define-key map (kbd "<down-mouse-3>") 'ampc-mouse-delete)
+ (define-key map (kbd "<mouse-3>") 'ampc-mouse-align-point)
map))
(defvar ampc-playlist-mode-map
(define-key map (kbd "d") 'ampc-delete)
(define-key map (kbd "<up>") 'ampc-up)
(define-key map (kbd "<down>") 'ampc-down)
+ (define-key map (kbd "<down-mouse-3>") 'ampc-mouse-delete)
+ (define-key map (kbd "<mouse-3>") 'ampc-mouse-align-point)
map))
(defvar ampc-playlists-mode-map
(define-key map (kbd "l") 'ampc-load)
(define-key map (kbd "r") 'ampc-rename-playlist)
(define-key map (kbd "d") 'ampc-delete-playlist)
+ (define-key map (kbd "<down-mouse-2>") 'ampc-mouse-load)
+ (define-key map (kbd "<mouse-2>") 'ampc-mouse-align-point)
+ (define-key map (kbd "<down-mouse-3>") 'ampc-mouse-delete-playlist)
+ (define-key map (kbd "<mouse-3>") 'ampc-mouse-align-point)
map))
(defvar ampc-tag-song-mode-map
(suppress-keymap map)
(define-key map (kbd "t") 'ampc-toggle-marks)
(define-key map (kbd "a") 'ampc-add)
+ (define-key map (kbd "<down-mouse-3>") 'ampc-mouse-add)
+ (define-key map (kbd "<mouse-3>") 'ampc-mouse-align-point)
map))
(defvar ampc-outputs-mode-map
(suppress-keymap map)
(define-key map (kbd "t") 'ampc-toggle-marks)
(define-key map (kbd "a") 'ampc-toggle-output-enabled)
+ (define-key map (kbd "<down-mouse-3>") 'ampc-mouse-toggle-output-enabled)
+ (define-key map (kbd "<mouse-3>") 'ampc-mouse-align-point)
map))
;;; **** menu
["Pause" ampc-toggle-play
:visible (and ampc-status
(equal (cdr (assq 'state ampc-status)) "play"))]
+ ["Stop" (lambda () (interactive) (ampc-toggle-play 4))
+ :visible (and ampc-status
+ (equal (cdr (assq 'state ampc-status)) "play"))]
+ ["Next" ampc-next]
+ ["Previous" ampc-previous]
"--"
["Clear playlist" ampc-clear]
["Shuffle playlist" ampc-shuffle]
"--"
["Increase volume" ampc-increase-volume]
["Decrease volume" ampc-decrease-volume]
+ ["Set volume" ampc-set-volume]
["Increase crossfade" ampc-increase-crossfade]
["Decrease crossfade" ampc-decrease-crossfade]
["Toggle repeat" ampc-toggle-repeat
["Toggle marks" ampc-toggle-marks
:visible (not (eq (car ampc-type) 'playlists))]))
+(defvar ampc-tool-bar-map
+ (let ((map (make-sparse-keymap)))
+ (tool-bar-local-item
+ "mpc/prev" 'ampc-previous 'previous map
+ :help "Previous")
+ (tool-bar-local-item
+ "mpc/play" 'ampc-toggle-play 'play map
+ :help "Play"
+ :visible '(and ampc-status
+ (not (equal (cdr (assq 'state ampc-status)) "play"))))
+ (tool-bar-local-item
+ "mpc/pause" 'ampc-toggle-play 'pause map
+ :help "Pause"
+ :visible '(and ampc-status
+ (equal (cdr (assq 'state ampc-status)) "play")))
+ (tool-bar-local-item
+ "mpc/stop" (lambda () (interactive) (ampc-toggle-play 4)) 'stop map
+ :help "Stop"
+ :visible '(and ampc-status
+ (equal (cdr (assq 'state ampc-status)) "play")))
+ (tool-bar-local-item
+ "mpc/next" 'ampc-next 'next map
+ :help "Next")
+ map))
+
;;; ** code
;;; *** macros
(defmacro ampc-with-buffer (type &rest body)
`(let* ((type- ,type)
(b (loop for b in ampc-buffers
when (with-current-buffer b
- (cond ((windowp type-)
- (eq (window-buffer type-)
- (current-buffer)))
- ((symbolp type-)
- (eq (car ampc-type) type-))
- (t
- (equal ampc-type type-))))
+ (etypecase type-
+ (window
+ (eq (window-buffer type-)
+ (current-buffer)))
+ (symbol
+ (eq (car ampc-type) type-))))
return b
end)))
(when b
when (get-text-property (point) 'updated)
do (delete-region (point) (1+ (line-end-position)))
else
- do (forward-line nil)
+ do (add-text-properties
+ (+ (point) 2)
+ (progn (forward-line nil)
+ (1- (point)))
+ '(mouse-face highlight))
end)
(goto-char point)
(ampc-align-point))
do (save-excursion
,@body))
(loop until (eobp)
- for index from 0 to (1- (prefix-numeric-value arg-))
+ for index from 0 to (1- (if (numberp arg-)
+ arg-
+ (prefix-numeric-value arg-)))
do (save-excursion
(goto-char (line-end-position))
,@body)
(define-derived-mode ampc-item-mode ampc-mode ""
nil)
-(define-derived-mode ampc-mode fundamental-mode "ampc"
+(define-derived-mode ampc-mode special-mode "ampc"
nil
(buffer-disable-undo)
- (setf buffer-read-only t
- truncate-lines ampc-truncate-lines
+ (set (make-local-variable 'tool-bar-map) ampc-tool-bar-map)
+ (setf truncate-lines ampc-truncate-lines
font-lock-defaults '((("^\\(\\*\\)\\(.*\\)$"
(1 'ampc-mark-face)
(2 'ampc-marked-face))
(defun ampc-quote (string)
(concat "\"" (replace-regexp-in-string "\"" "\\\"" string) "\""))
-(defun ampc-on-p ()
- (and ampc-connection
- (member (process-status ampc-connection) '(open run))))
-
(defun ampc-in-ampc-p ()
(when (ampc-on-p)
ampc-type))
(loop for d in (reverse data)
do (ampc-add-impl (cdr (assoc "file" d)))))))
-(defun* ampc-skip (N &aux (song (cdr-safe (assq 'song ampc-status))))
- (when song
- (ampc-send-command 'play nil (max 0 (+ (string-to-number song) N)))))
+(defun* ampc-skip (N)
+ (ampc-send-command 'play
+ nil
+ (let ((N N))
+ (lambda ()
+ (let ((song (cdr-safe (assq 'song ampc-status))))
+ (unless song
+ (throw 'skip nil))
+ (max 0 (+ (string-to-number song) N))))))
+ (ampc-send-command 'currentsong))
(defun* ampc-find-current-song
(limit &aux (point (point)) (song (cdr-safe (assq 'song ampc-status))))
(narrow-to-region (max point (point)) (min limit (line-end-position)))
(search-forward-regexp "\\(?1:\\(\\`\\*\\)?\\)\\(?2:.*\\)$"))))
-(defun ampc-set-volume (arg func)
- (when (or arg ampc-status)
+(defun ampc-setvol (arg &optional func)
+ (when ampc-status
+ (when arg
+ (setf arg (prefix-numeric-value arg)))
(ampc-send-command
'setvol
nil
- (or (and arg (prefix-numeric-value arg))
- (max (min (funcall func
+ (max (min (if func
+ (funcall func
(string-to-number
(cdr (assq 'volume ampc-status)))
- 5)
- 100)
- 0)))))
+ (or arg ampc-volume-step))
+ arg)
+ 100)
+ 0))
+ (ampc-send-command 'status)))
(defun ampc-set-crossfade (arg func)
(when (or arg ampc-status)
((> (prefix-numeric-value arg) 0) 1)
(t 0)))))
-(defun ampc-playlist ()
+(defun ampc-playlist (&optional at-point)
(ampc-with-buffer 'playlists
- (if (search-forward-regexp "^* \\(.*\\)$" nil t)
+ (if (and (not at-point)
+ (search-forward-regexp "^* \\(.*\\)$" nil t))
(match-string 1)
(unless (eobp)
(buffer-substring-no-properties
(move-beginning-of-line nil)
(forward-char 2)))
-(defun ampc-pad (alist)
- (loop for (offset . data) in alist
+(defun* ampc-pad (tabs &optional (sub 0))
+ (loop for tab in tabs
+ for offset in ampc-tab-offsets
+ do (setf offset (- offset sub))
with first = t
with current-offset = 0
when (<= current-offset offset)
concat " "
and do (incf current-offset)
end
- concat data
- do (setf current-offset (+ current-offset (length data))
- first nil)))
+ concat tab
+ do (incf current-offset (length tab))
+ (setf first nil)))
(defun ampc-update-header ()
- (if (eq (car ampc-type) 'status)
- (setf header-line-format nil)
- (setf header-line-format
+ (setf header-line-format
+ (unless (eq (car ampc-type) 'status)
(concat
+ (when ampc-dirty
+ " [ Updating... ]")
(make-string (floor (fringe-columns 'left t)) ? )
(ecase (car ampc-type)
(tag
" Playlists")
(t
(ampc-pad (loop for p in (plist-get (cdr ampc-type) :properties)
- collect `(,(or (plist-get (cdr p) :offset) 2) .
- ,(or (plist-get (cdr p) :title)
- (car p)))))))
- (when ampc-dirty
- " [ Updating... ]")))))
+ collect (or (plist-get (cdr p) :title)
+ (car p))))))))))
(defun ampc-set-dirty (tag-or-dirty &optional dirty)
(if (or (null tag-or-dirty) (eq tag-or-dirty t))
nil
(get-text-property (point) 'data))
(ampc-with-buffer 'playlist
- (delete-region (point-min) (point-max))
+ (erase-buffer)
(ampc-set-dirty nil)))))
(defun ampc-send-command-impl (command)
(when ampc-debug
- (message (concat "ampc: " command)))
+ (message "ampc: -> %s" command))
(process-send-string ampc-connection (concat command "\n")))
(defun ampc-send-command (command &optional unique &rest args)
(setf ampc-outstanding-commands
(nconc (if unique
ampc-outstanding-commands
- (remove command ampc-outstanding-commands))
+ (delete command ampc-outstanding-commands))
`(,command))))
(defun ampc-send-next-command ()
+ (loop while ampc-outstanding-commands
+ for command = (replace-regexp-in-string
+ "^.*?-" ""
+ (symbol-name (caar ampc-outstanding-commands)))
+ do
+ (loop until (catch 'skip
+ (ampc-send-command-impl
+ (concat command
+ (loop for a in (cdar ampc-outstanding-commands)
+ concat " "
+ do (when (functionp a)
+ (setf a (funcall a)))
+ concat (typecase a
+ (integer (number-to-string a))
+ (t a)))))
+ t)
+ do (pop ampc-outstanding-commands))
+ while (and ampc-outstanding-commands (not (eq (intern command) 'idle)))
+ while
+ (let ((member (member (intern command) ampc-synchronous-commands)))
+ (when (or (and (not (eq (car ampc-synchronous-commands) t)) member)
+ (and (eq (car ampc-synchronous-commands) t) (not member)))
+ (loop with head = ampc-outstanding-commands
+ with ampc-no-implicit-next-dispatch = t
+ while (eq head ampc-outstanding-commands)
+ do (accept-process-output ampc-connection 0 100))
+ t)))
(unless ampc-outstanding-commands
- (ampc-send-command 'idle))
- (ampc-send-command-impl (concat (symbol-name (caar ampc-outstanding-commands))
- (loop for a in
- (cdar ampc-outstanding-commands)
- concat " "
- concat (cond ((integerp a)
- (number-to-string a))
- (t a))))))
+ (ampc-send-command 'idle)
+ (ampc-send-next-command)))
(defun ampc-tree< (a b)
(string< (car a) (car b)))
do (ampc-insert
(ampc-pad
(loop for (p . v) in (plist-get (cdr ampc-type) :properties)
- collect `(,(- (or (plist-get v :offset) 2) 2)
- . ,(or (cdr-safe (assoc p song)) ""))))
+ collect (or (cdr-safe (assoc p song)) ""))
+ 2)
`((,song))))))
-(defun* ampc-narrow-entry (&optional (delimiter "file"))
- (narrow-to-region (move-beginning-of-line nil)
- (or (progn (goto-char (line-end-position))
- (when (search-forward-regexp
- (concat "^" (regexp-quote delimiter) ": ")
- nil
- t)
- (move-beginning-of-line nil)
- (1- (point))))
- (point-max))))
+(defun* ampc-narrow-entry (&optional (delimiter "file") &aux result)
+ (narrow-to-region
+ (move-beginning-of-line nil)
+ (or (progn (goto-char (line-end-position))
+ (when (setf result (search-forward-regexp
+ (concat "^" (regexp-quote delimiter) ": ")
+ nil
+ t))
+ (move-beginning-of-line nil)
+ (1- (point))))
+ (point-max)))
+ result)
(defun ampc-get-window (type)
(loop for w in (ampc-windows)
(with-current-buffer data-buffer
(loop
for i from 0
- while (search-forward-regexp "^file: " nil t)
+ with next
+ while (or (when next (goto-char next) t)
+ (search-forward-regexp "^file: " nil t))
do (save-restriction
- (ampc-narrow-entry)
+ (setf next (ampc-narrow-entry))
(let ((file (ampc-extract "file"))
- (text
- (ampc-pad
- (loop for (tag . tag-properties) in properties
- collect `(,(- (or (plist-get tag-properties
- :offset)
- 2)
- 2)
- . ,(or (ampc-extract tag)
- "[Not Specified]"))))))
+ (pad-data (loop for (tag . tag-properties) in properties
+ collect (or (ampc-extract tag)
+ "[Not Specified]"))))
(ampc-with-buffer 'playlist
- (ampc-insert text
+ (ampc-insert (ampc-pad pad-data 2)
`(("file" . ,file)
(index . ,i))
(lambda (a b)
(setf properties (plist-get (cdr ampc-type) :properties))
(with-current-buffer data-buffer
(loop
- while (search-forward-regexp "^outputid: " nil t)
+ with next
+ while (or (when next (goto-char next) t)
+ (search-forward-regexp "^outputid: " nil t))
do (save-restriction
- (ampc-narrow-entry "outputid")
+ (setf next (ampc-narrow-entry "outputid"))
(let ((outputid (ampc-extract "outputid"))
- (outputenabled (ampc-extract "outputenabled"))
- (text
- (ampc-pad
- (loop for (tag . tag-properties) in properties
- collect `(,(- (or (plist-get tag-properties :offset)
- 2)
- 2)
- . ,(ampc-extract tag))))))
+ (outputenabled (ampc-extract "outputenabled")))
(ampc-with-buffer 'outputs
- (ampc-insert text `(("outputid" . ,outputid)
- ("outputenabled" . ,outputenabled))))))))))
+ (ampc-insert (ampc-pad
+ (loop for (tag . tag-properties) in properties
+ collect (with-current-buffer data-buffer
+ (ampc-extract tag)))
+ 2)
+ `(("outputid" . ,outputid)
+ ("outputenabled" . ,outputenabled))))))))))
+
+(defun* ampc-mini-impl (&aux songs)
+ (loop with next
+ while (or (when next (goto-char next) t)
+ (search-forward-regexp "^file: " nil t))
+ for entry = (save-restriction
+ (setf next (ampc-narrow-entry))
+ `(,(concat (ampc-extract "Title") " - "
+ (ampc-extract "Artist"))
+ . ,(string-to-number (ampc-extract "Pos"))))
+ do (loop with mentry = `(,(car entry) . ,(cdr entry))
+ for index from 2
+ while (assoc (car mentry) songs)
+ do (setf (car mentry) (concat (car entry)
+ " (" (int-to-string index) ")"))
+ finally do (push mentry songs)))
+ (unless songs
+ (message "No song in the playlist")
+ (return-from ampc-mini-impl))
+ (let ((song (assoc (let ((inhibit-quit t))
+ (prog1
+ (with-local-quit
+ (completing-read "Song to play: " songs nil t))
+ (setf quit-flag nil)))
+ songs)))
+ (when song
+ (ampc-play-this (cdr song)))))
(defun* ampc-fill-current-playlist (&aux properties)
(ampc-fill-skeleton 'current-playlist
(setf properties (plist-get (cdr ampc-type) :properties))
(with-current-buffer data-buffer
(loop
- while (search-forward-regexp "^file: " nil t)
+ with next
+ while (or (when next (goto-char next) t)
+ (search-forward-regexp "^file: " nil t))
do (save-restriction
- (ampc-narrow-entry)
+ (setf next (ampc-narrow-entry))
(let ((file (ampc-extract "file"))
- (pos (ampc-extract "Pos"))
- (text
- (ampc-pad
- (loop for (tag . tag-properties) in properties
- collect `(,(- (or (plist-get tag-properties :offset)
- 2)
- 2)
- . ,(or (ampc-extract tag)
- "[Not Specified]"))))))
+ (pos (ampc-extract "Pos")))
(ampc-with-buffer 'current-playlist
- (ampc-insert text
- `(("file" . ,file)
- ("Pos" . ,(string-to-number pos)))
- (lambda (a b)
- (let ((p1 (cdr (assoc "Pos" a)))
- (p2 (cdr (assoc "Pos" b))))
- (cond ((< p1 p2) 'insert)
- ((eq p1 p2)
- (if (equal (cdr (assoc "file" a))
- (cdr (assoc "file" b)))
- 'update
- 'insert))
- (t (- p1 p2)))))))))))))
+ (ampc-insert
+ (ampc-pad
+ (loop for (tag . tag-properties) in properties
+ collect (or (with-current-buffer data-buffer
+ (ampc-extract tag))
+ "[Not Specified]"))
+ 2)
+ `(("file" . ,file)
+ ("Pos" . ,(string-to-number pos)))
+ (lambda (a b)
+ (let ((p1 (cdr (assoc "Pos" a)))
+ (p2 (cdr (assoc "Pos" b))))
+ (cond ((< p1 p2) 'insert)
+ ((eq p1 p2)
+ (if (equal (cdr (assoc "file" a))
+ (cdr (assoc "file" b)))
+ 'update
+ 'insert))
+ (t (- p1 p2)))))))))))))
(defun ampc-fill-playlists ()
(ampc-fill-skeleton 'playlists
(ampc-insert playlist playlist))))))
(defun ampc-yield ()
- (setf ampc-yield (1+ ampc-yield))
- (ampc-fill-status))
+ (incf ampc-yield)
+ (ampc-fill-status)
+ (redisplay t))
(defun ampc-fill-status ()
(ampc-with-buffer 'status
- (delete-region (point-min) (point-max))
+ (erase-buffer)
(funcall (or (plist-get (cadr ampc-type) :filler)
(lambda (_)
- (insert (ampc-status) "\n")))
+ (insert (ampc-status t) "\n")))
ampc-status)
(ampc-set-dirty nil)))
(ampc-fill-playlists))
(playlistinfo
(ampc-fill-current-playlist))
+ (mini-playlistinfo
+ (ampc-mini-impl))
+ (mini-currentsong
+ (ampc-status))
(listallinfo
(ampc-fill-internal-db nil))
(outputs
(with-current-buffer (process-buffer ampc-connection)
(when string
(when ampc-debug
- (message "ampc: -> %s" string))
+ (message "ampc: <- %s" string))
(goto-char (process-mark ampc-connection))
(insert string)
(set-marker (process-mark ampc-connection) (point)))
(save-excursion
(goto-char (point-min))
(let ((success))
- (if (or (and (search-forward-regexp
- "^ACK \\[\\(.*\\)\\] {.*} \\(.*\\)\n\\'"
- nil
- t)
- (message "ampc command error: %s (%s)"
- (match-string 2)
- (match-string 1))
- t)
+ (if (or (progn
+ (when (search-forward-regexp
+ "^ACK \\[\\(.*\\)\\] {.*} \\(.*\\)\n\\'"
+ nil
+ t)
+ (message "ampc command error: %s (%s)"
+ (match-string 2)
+ (match-string 1))
+ t))
(and (search-forward-regexp "^OK\\(.*\\)\n\\'" nil t)
(setf success t)))
(progn
(goto-char (point-min))
(ampc-handle-command (if success (match-string 1) 'error)))
(delete-region (point-min) match-end))
- (ampc-send-next-command))
- (ampc-handle-command 'running))))))
+ (unless ampc-no-implicit-next-dispatch
+ (ampc-send-next-command))))
+ (ampc-handle-command 'running)))))
;;; **** window management
(defun ampc-windows (&optional unordered)
b)
w))))))
+(defun* ampc-set-tab-offsets
+ (&rest properties &aux (min 2) (optional-padding 0))
+ (loop for (title . props) in properties
+ for min- = (plist-get props :min)
+ do (incf min (or (plist-get props :width) min-))
+ when min-
+ do (incf optional-padding (- (plist-get props :max) min-))
+ end)
+ (setf ampc-tab-offsets nil)
+ (loop for (title . props) in properties
+ with offset = 2
+ do (add-to-list 'ampc-tab-offsets offset t)
+ (incf offset (or (plist-get props :width)
+ (let ((min- (plist-get props :min))
+ (max (plist-get props :max)))
+ (if (>= min (window-width))
+ min-
+ (min max
+ (+ min-
+ (floor (* (/ (float (- max min-))
+ optional-padding)
+ (- (window-width)
+ min)))))))))))
+
(defun* ampc-configure-frame-1 (split &aux (split-type (car split)))
(if (member split-type '(vertical horizontal))
(let* ((sizes))
(window-height))
with rest = length
with rest-car
- for subsplit in (cdr split)
- for s = (car subsplit)
- if (equal s 1.0)
+ for (size . subsplit) in (cdr split)
+ if (equal size 1.0)
do (push t sizes)
and do (setf rest-car sizes)
else
- do (let ((l (if (integerp s) s (floor (* s length)))))
+ do (let ((l (if (integerp size) size (floor (* size length)))))
(setf rest (- rest l))
(push l sizes))
finally do (setf (car rest-car) rest))
(status
(pop-to-buffer-same-window (get-buffer-create "*ampc Status*"))
(ampc-mode)))
- (destructuring-bind (&key (dedicated t) (mode-line t) &allow-other-keys)
+ (destructuring-bind
+ (&key (properties nil) (dedicated t) (mode-line t) &allow-other-keys)
(cdr split)
- (setf (window-dedicated-p (selected-window)) dedicated)
- (unless mode-line
- (setf mode-line-format nil)))
- (setf ampc-type split)
+ (if properties
+ (apply 'ampc-set-tab-offsets properties)
+ (setf ampc-tab-offsets '(2)))
+ (setf ampc-type split
+ (window-dedicated-p (selected-window)) dedicated
+ mode-line-format (when mode-line
+ (default-value 'mode-line-format))))
(add-to-list 'ampc-all-buffers (current-buffer))
(push `(,(or (plist-get (cdr split) :id)
(if (eq (car ampc-type) 'song) 9998 9999))
(delete-other-windows))
(loop with live-window = nil
for w in (nreverse (ampc-windows t))
- if (window-live-p w)
- if (not live-window)
- do (setf live-window w)
- else
- do (delete-window w)
- end
- end
+ do (when (window-live-p w)
+ (if (not live-window)
+ (setf live-window w)
+ (delete-window w)))
finally do (if live-window (select-window live-window))))
(setf ampc-buffers nil)
(ampc-configure-frame-1 split)
(setf ampc-buffers-unordered (mapcar 'cdr ampc-buffers)
ampc-buffers (mapcar 'cdr (sort ampc-buffers
(lambda (a b) (< (car a) (car b))))))
+ ;; fill the song, current-playlist and outputs buffers again as the tab
+ ;; offsets might have changed
+ (ampc-with-buffer 'song
+ (erase-buffer))
+ (ampc-with-buffer 'current-playlist
+ (erase-buffer))
+ (ampc-with-buffer 'outputs
+ (erase-buffer))
(ampc-update))
+(defun ampc-mouse-play-this (event)
+ (interactive "e")
+ (select-window (posn-window (event-end event)))
+ (goto-char (posn-point (event-end event)))
+ (ampc-play-this))
+
+(defun ampc-mouse-delete (event)
+ (interactive "e")
+ (select-window (posn-window (event-end event)))
+ (goto-char (posn-point (event-end event)))
+ (ampc-delete 1))
+
+(defun ampc-mouse-add (event)
+ (interactive "e")
+ (select-window (posn-window (event-end event)))
+ (goto-char (posn-point (event-end event)))
+ (ampc-add-impl))
+
+(defun ampc-mouse-delete-playlist (event)
+ (interactive "e")
+ (select-window (posn-window (event-end event)))
+ (goto-char (posn-point (event-end event)))
+ (ampc-delete-playlist t))
+
+(defun ampc-mouse-load (event)
+ (interactive "e")
+ (select-window (posn-window (event-end event)))
+ (goto-char (posn-point (event-end event)))
+ (ampc-load t))
+
+(defun ampc-mouse-toggle-output-enabled (event)
+ (interactive "e")
+ (select-window (posn-window (event-end event)))
+ (goto-char (posn-point (event-end event)))
+ (ampc-toggle-output-enabled 1))
+
+(defun* ampc-mouse-toggle-mark (event &aux buffer-read-only)
+ (interactive "e")
+ (let ((window (posn-window (event-end event))))
+ (when (with-selected-window window
+ (goto-char (posn-point (event-end event)))
+ (unless (eobp)
+ (move-beginning-of-line nil)
+ (ampc-mark-impl (not (eq (char-after) ?*)) 1)
+ t))
+ (select-window window))))
+
+(defun ampc-mouse-align-point (event)
+ (interactive "e")
+ (select-window (posn-window (event-end event)))
+ (goto-char (posn-point (event-end event)))
+ (ampc-align-point))
+
;;; *** interactives
(defun* ampc-unmark-all (&aux buffer-read-only)
"Remove all marks."
(assert (ampc-in-ampc-p))
(ampc-mark-impl nil arg))
+(defun ampc-set-volume (&optional arg)
+ "Set volume to ARG percent.
+If ARG is nil, read ARG from minibuffer."
+ (interactive "P")
+ (assert (ampc-on-p))
+ (ampc-setvol (or arg (read-number "Volume: "))))
+
(defun ampc-increase-volume (&optional arg)
- "Decrease volume.
-With prefix argument ARG, set volume to ARG percent."
+ "Increase volume by prefix argument ARG or, if ARG is nil,
+`ampc-volume-step'."
(interactive "P")
(assert (ampc-on-p))
- (ampc-set-volume arg '+))
+ (ampc-setvol arg '+))
(defun ampc-decrease-volume (&optional arg)
- "Decrease volume.
-With prefix argument ARG, set volume to ARG percent."
+ "Decrease volume by prefix argument ARG or, if ARG is nil,
+`ampc-volume-step'."
(interactive "P")
(assert (ampc-on-p))
- (ampc-set-volume arg '-))
+ (ampc-setvol arg '-))
(defun ampc-increase-crossfade (&optional arg)
"Increase crossfade.
(interactive "P")
(ampc-toggle-state 'random arg))
-(defun ampc-play-this ()
- "Play selected song."
- (interactive)
- (assert (ampc-in-ampc-p))
- (unless (eobp)
- (ampc-send-command 'play nil (1- (line-number-at-pos)))
+(defun ampc-play-this (&optional arg)
+ "Play selected song.
+With prefix argument ARG, play the ARG'th song located at the
+zero-indexed position of the current playlist."
+ (interactive "P")
+ (assert (and (ampc-on-p) (or arg (ampc-in-ampc-p))))
+ (if (not arg)
+ (unless (eobp)
+ (ampc-send-command 'play nil (1- (line-number-at-pos)))
+ (ampc-send-command 'pause nil 0))
+ (ampc-send-command 'play nil arg)
(ampc-send-command 'pause nil 0)))
(defun* ampc-toggle-play
(&optional arg &aux (state (cdr-safe (assq 'state ampc-status))))
"Toggle play state.
-If mpd does not play a song already, start playing the song at
+If MPD does not play a song already, start playing the song at
point if the current buffer is the playlist buffer, otherwise
start at the beginning of the playlist.
(ampc-send-command 'rename nil (ampc-playlist) new-name)
(error "No playlist selected")))
-(defun ampc-load ()
- "Load selected playlist in the current playlist."
+(defun ampc-load (&optional at-point)
+ "Load selected playlist in the current playlist.
+If optional argument AT-POINT is non-nil (or if no playlist is
+selected), use playlist at point rather than the selected one."
(interactive)
(assert (ampc-in-ampc-p))
- (if (ampc-playlist)
- (ampc-send-command 'load nil (ampc-quote (ampc-playlist)))
- (error "No playlist selected")))
+ (if (ampc-playlist at-point)
+ (ampc-send-command 'load nil (ampc-quote (ampc-playlist at-point)))
+ (if at-point
+ (error "No playlist at point")
+ (error "No playlist selected"))))
(defun ampc-toggle-output-enabled (&optional arg)
"Toggle the next ARG outputs.
(defun ampc-delete (&optional arg)
"Delete the next ARG songs from the playlist.
-If ARG is omitted, use the selected entries."
+If ARG is omitted, use the selected entries. If ARG is non-nil,
+all marks after point are removed nontheless."
(interactive "P")
(assert (ampc-in-ampc-p))
(let ((point (point)))
(ampc-with-selection arg
(ampc-add-impl)))
-(defun ampc-status ()
- "Display the information that is displayed in the status window."
+(defun* ampc-status (&optional no-print)
+ "Display and return the information that is displayed in the status window.
+If optional argument NO-PRINT is non-nil, just return the text.
+If NO-PRINT is nil, the display may be delayed if ampc does not
+have enough information yet."
(interactive)
(assert (ampc-on-p))
+ (unless (or ampc-status no-print)
+ (ampc-send-command 'status t)
+ (ampc-send-command 'mini-currentsong t)
+ (return-from ampc-status))
(let* ((flags (mapconcat
'identity
(loop for (f . n) in '((repeat . "Repeat")
"|"))
(state (cdr (assq 'state ampc-status)))
(status (concat "State: " state
- (when ampc-yield
+ (when (and ampc-yield no-print)
(concat (make-string (- 10 (length state)) ? )
(nth (% ampc-yield 4) '("|" "/" "-" "\\"))))
"\n"
"Crossfade: " (cdr (assq 'xfade ampc-status))
(unless (equal flags "")
(concat "\n" flags)))))
- (when (called-interactively-p 'interactive)
+ (unless no-print
(message "%s" status))
status))
-(defun ampc-delete-playlist ()
- "Delete selected playlist."
+(defun ampc-delete-playlist (&optional at-point)
+ "Delete selected playlist.
+If optional argument AT-POINT is non-nil (or if no playlist is
+selected), use playlist at point rather than the selected one."
(interactive)
(assert (ampc-in-ampc-p))
- (ampc-with-selection nil
- (let ((name (get-text-property (point) 'data)))
- (when (y-or-n-p (concat "Delete playlist " name "?"))
- (ampc-send-command 'rm nil (ampc-quote name))))))
+ (if (ampc-playlist at-point)
+ (when (y-or-n-p (concat "Delete playlist " (ampc-playlist at-point) "?"))
+ (ampc-send-command 'rm nil (ampc-quote (ampc-playlist at-point))))
+ (if at-point
+ (error "No playlist at point")
+ (error "No playlist selected"))))
(defun ampc-store (name)
"Store current playlist as NAME.
"Select the current playlist window and move point to the current song."
(interactive)
(assert (ampc-in-ampc-p))
- (when song
- (ampc-with-buffer 'current-playlist
- no-se
- (select-window (ampc-get-window 'current-playlist))
+ (ampc-with-buffer 'current-playlist
+ no-se
+ (select-window (ampc-get-window 'current-playlist))
+ (when song
(goto-char (point-min))
- (forward-line (string-to-number song))
- (ampc-align-point))))
+ (forward-line (string-to-number song)))
+ (ampc-align-point)))
(defun ampc-previous-line (&optional arg)
"Go to previous ARG'th entry in the current buffer.
(defun* ampc-suspend (&optional (run-hook t))
"Suspend ampc.
This function resets the window configuration, but does not close
-the connection to mpd or destroy the internal cache of ampc.
+the connection to MPD or destroy the internal cache of ampc.
This means subsequent startups of ampc will be faster."
(interactive)
(when ampc-working-timer
(cancel-timer ampc-working-timer))
(loop with found-window
for w in (nreverse (ampc-windows t))
- when (window-live-p w)
- when found-window
- do (delete-window w)
- else
- do (setf found-window t
- (window-dedicated-p w) nil)
- end
- end)
+ do (when (window-live-p w)
+ (if found-window
+ (delete-window w)
+ (setf found-window t
+ (window-dedicated-p w) nil))))
(loop for b in ampc-all-buffers
- when (buffer-live-p b)
- do (kill-buffer b)
- end)
+ do (when (buffer-live-p b)
+ (kill-buffer b)))
(setf ampc-buffers nil
ampc-all-buffers nil
ampc-working-timer nil)
(when run-hook
(run-hooks 'ampc-suspend-hook)))
+(defun ampc-mini ()
+ "Select song to play via `completing-read'."
+ (interactive)
+ (assert (ampc-on-p))
+ (ampc-send-command 'mini-playlistinfo t))
+
(defun ampc-quit (&optional arg)
"Quit ampc.
-If called with a prefix argument ARG, kill the mpd instance that
+If called with a prefix argument ARG, kill the MPD instance that
ampc is connected to."
(interactive "P")
(when (ampc-on-p)
(run-hooks 'ampc-quit-hook))
;;;###autoload
-(defun ampc (&optional host port)
+(defun ampc-suspended-p ()
+ "Return non-nil if ampc is suspended."
+ (interactive)
+ (and (ampc-on-p)
+ (not ampc-buffers)))
+
+;;;###autoload
+(defun ampc-on-p ()
+ "Return non-nil if ampc is connected to the daemon."
+ (interactive)
+ (and ampc-connection (memq (process-status ampc-connection) '(open run))))
+
+;;;###autoload
+(defun ampc (&optional host port suspend)
"ampc is an asynchronous client for the MPD media player.
This function is the main entry point for ampc.
-Non-interactively, HOST and PORT specify the MPD instance to
-connect to. The values default to localhost:6600."
- (interactive "MHost (localhost): \nMPort (6600): ")
+HOST and PORT specify the MPD instance to connect to. The values
+default to the ones specified in `ampc-default-server'."
+ (interactive)
+ (unless (byte-code-function-p (symbol-function 'ampc))
+ (message "You should byte-compile ampc"))
(run-hooks 'ampc-before-startup-hook)
- (when (or (not host) (equal host ""))
- (setf host "localhost"))
- (when (or (not port) (equal port ""))
- (setf port 6600))
+ (unless host
+ (setf host (or (car ampc-default-server)
+ (read-string "Host: "))))
+ (unless port
+ (setf port (or (cdr ampc-default-server)
+ (read-string "Port: "))))
(when (and ampc-connection
(or (not (equal host ampc-host))
(not (equal port ampc-port))
(unless ampc-connection
(let ((connection (open-network-stream "ampc"
(with-current-buffer
- (get-buffer-create " *mpc*")
- (delete-region (point-min)
- (point-max))
+ (get-buffer-create " *ampc*")
+ (erase-buffer)
(current-buffer))
host
port
(set-process-filter ampc-connection 'ampc-filter)
(set-process-query-on-exit-flag ampc-connection nil)
(setf ampc-outstanding-commands '((setup))))
- (ampc-configure-frame (cddar ampc-views))
+ (if suspend
+ (ampc-update)
+ (ampc-configure-frame (cddar ampc-views)))
(run-hooks 'ampc-connected-hook)
+ (when suspend
+ (ampc-suspend))
(ampc-filter (process-buffer ampc-connection) nil))
(provide 'ampc)