Better faces, track selection without playing.
[emus.git] / emus.el
diff --git a/emus.el b/emus.el
index eb3d566..315ca6e 100644 (file)
--- a/emus.el
+++ b/emus.el
   "Name of (and, optionally, path to) mpg123 binary."
   :type '(string))
 
+(defface emus-artist
+  '((t :inherit font-lock-keyword-face :background "#333"))
+  "Face used for artist names in browser.")
+
+(defface emus-album
+  '((t :inherit font-lock-function-name-face :background "#222"))
+  "Face used for album names in browser.")
+
+(defface emus-track
+  '((t :inherit font-lock-string-face))
+  "Face used for track titles in browser.")
+
+(defface emus-track-current
+  '((t :inherit font-lock-string-face :inverse-video t))
+  "Face used for track titles in browser.")
+
+(defface emus-cursor
+  '((t :inherit bold))
+  "Face used for current track cursor")
+
 ;;; Library
 ;;
 
-(defvar emus-library nil
-  "Emus audio library.")
-
 (defun emus-get-audio-files ()
   "Get all mp3 files in main emus directory."
   (directory-files-recursively emus-directory ".*\\.mp3"))
 
-(defvar emus-records nil)
+(defvar emus-records nil
+  "Emus audio library.")
 
 (defun emus-make-record (filename tagstr)
   (let ((artist "")
@@ -51,7 +69,7 @@
          (found-artist (setq artist found-artist))
          (found-album (setq album found-album))
          (found-title (setq title found-title)))))
-    (list artist album title filename)))
+    (vector artist album title filename nil)))
 
 (defun emus-record-artist (record)
   (elt record 0))
 (defun emus-record-file (record)
   (elt record 3))
 
-(defun emus-update-records (then)
+(defun emus-record-browser-pos (record)
+  (elt record 4))
+
+(defun emus-set-record-browser-pos (record pos)
+  (aset record 4 pos))
+
+(defun emus-update-records ()
+  (interactive)
+  (emus-suspend-cp)
+  (setq emus-state 'stopped)
   (let ((proc (emus-get-process))
         (tagstr "")
         (filenames (emus-get-audio-files)))
                                  (if filenames
                                      (emus-send-cmd "lp" (car filenames))
                                    (set-process-filter proc nil)
-                                   (funcall then)))))
+                                   (setq emus-records (reverse emus-records))
+                                   (emus-sort-records)
+                                   (emus-render-records)
+                                   (emus-resume-cp)))))
     (emus-send-cmd "lp" (car filenames))))
 
+(defun emus-sort-records ()
+  (sort emus-records
+        (lambda (r1 r2)
+          (let ((artist1 (emus-record-artist r1))
+                (artist2 (emus-record-artist r2)))
+            (if (string= artist1 artist2)
+                (let ((album1 (emus-record-album r1))
+                      (album2 (emus-record-album r2)))
+                  (string< album1 album2))
+              (string< artist1 artist2))))))        
+
 ;;; mpg123 process
 ;;
 
                              nil))))
     (if emus-process
         emus-process
-      (make-process :name "emus-process"
-                    ;; :buffer (get-buffer-create "*emus-process*")
-                    :command `(,emus-mpg123-program "-R")))))
+      (let ((proc
+             (make-process :name "emus-process"
+                           ;; :buffer (get-buffer-create "*emus-process*")
+                           :command `(,emus-mpg123-program "-R"))))
+        (process-send-string proc "silence\n")
+        proc))))
+                    
 
 (defun emus-send-cmd (cmd &rest args)
   (process-send-string (emus-get-process)
                         (seq-reduce (lambda (s1 s2) (concat s1 " " s2)) args cmd)
                         "\n")))
 
+(defun emus-send-and-process (respfun predfun cmd &rest args)
+  (let ((respstr ""))
+    (set-process-filter (emus-get-process)
+                        (lambda (proc string)
+                          (setq respstr (concat respstr string))
+                          (when (funcall predfun respstr)
+                            (set-process-filter proc nil)
+                            (funcall respfun respstr))))
+    (apply #'emus-send-cmd cmd args)))
+
+
 ;;; Playback
 ;;
 
-(defvar emus-currently-playing nil)
+(defvar emus-current-record nil)
+(defvar emus-state 'stopped)
+(defvar emus-continuous-playback t)
+
+(defun emus-suspend-cp ()
+  (setq emus-continuous-playback nil))
+
+(defun emus-resume-cp ()
+  (setq emus-continuous-playback t)
+  (set-process-filter (emus-get-process)
+                      (lambda (proc string)
+                        (and emus-continuous-playback
+                             (eq emus-state 'playing)
+                             (string-suffix-p "@P 0\n" string)
+                             (emus-play-next)))))
 
 (defun emus-play-record (record)
-  (setq emus-currently-playing record)
-  (emus-send-cmd "l" (emus-record-file record)))
+  "Set RECORD as current and start playing."
+  (let ((old-record emus-current-record))
+    (emus-send-cmd "l" (emus-record-file record))
+    (setq emus-state 'playing)
+    (setq emus-current-record record)
+    (emus-update-record old-record)
+    (emus-update-record record)
+    (emus-resume-cp)))
+
+(defun emus-select-record (record)
+  "Set RECORD as current, but do not start playing."
+  (let ((old-record emus-current-record))
+    (setq emus-state 'stopped)
+    (setq emus-current-record record)
+    (emus-update-record old-record)
+    (emus-update-record record)
+    (emus-send-cmd "o")
+    (emus-resume-cp)))
 
 (defun emus-stop ()
+  "Stop playback of the current record."
   (interactive)
-  (emus-send-cmd "s")
-  (setq emus-currently-playing nil))
+  (setq emus-state 'stopped)
+  (emus-update-record emus-current-record)
+  (emus-send-cmd "s"))
 
 (defun emus-playpause ()
   (interactive)
-  (emus-send-cmd "p"))
-
-
-(defun emus-volume (pct)
+  (when emus-current-record
+    (if (eq emus-state 'stopped)
+        (emus-play-record emus-current-record)
+      (emus-send-cmd "p")
+      (pcase emus-state
+        ((or 'paused 'stopped) (setq emus-state 'playing))
+        ('playing (setq emus-state 'paused)))
+      (unless (eq emus-state 'paused)))
+    (emus-update-record emus-current-record)))
+
+(defun emus-set-volume (pct)
   (emus-send-cmd "v" (number-to-string pct)))
 
-(defvar emus-current-volume 10)
+(defvar emus-current-volume 100)
 
 (defun emus-volume-delta (delta)
   (setq emus-current-volume (max 0 (min 100 (+ emus-current-volume delta))))
-  (emus-volume emus-current-volume))
+  (emus-set-volume emus-current-volume))
 
 (defun emus-volume-up ()
   (interactive)
   (interactive)
   (emus-volume-delta -10))
 
+(defun emus-play-nearby (offset)
+  (let ((idx (seq-position emus-records emus-current-record)))
+    (if idx
+        (let ((next-record (elt emus-records (+ idx offset))))
+          (if next-record
+              (if (eq emus-state 'playing)
+                  (emus-play-record next-record)
+                (emus-select-record next-record))
+            (error "Track does not exist")))
+      (error "No track is currently selected."))))
+
+(defun emus-play-next ()
+  (interactive)
+  (emus-play-nearby 1))
+
+(defun emus-play-prev ()
+  (interactive)
+  (emus-play-nearby -1))
+
+(defun emus-display-status ()
+  (interactive)
+  (message
+   (concat "Emus: Volume %d%%"
+           (pcase emus-state
+             ('stopped " [Stopped]")
+             ('paused " [Paused]")
+             ('playing " [Playing]")
+             (_ ""))
+           (if emus-current-record
+               (format " - %.30s (%.20s)"
+                       (emus-record-title emus-current-record)
+                       (emus-record-artist emus-current-record))
+             ""))
+   emus-current-volume))
+
 
 ;;; Browser
 ;;
 
-(defun emus-render-record (record)
-   (insert-text-button
-    (concat
-     (propertize (format "%-20.20s" (emus-record-artist record))
-                 'face 'font-lock-keyword-face)
-     (propertize (format "%  -20.20s" (emus-record-album record))
-                 'face 'font-lock-function-name-face)
-     (propertize (format "  %s" (emus-record-title record))
-                 'face 'font-lock-string-face))
-    'action #'emus-click-record
-    'follow-link t
-    'emus-record record)
-  (insert "\n"))
+(defun emus-insert-record (record &optional prev-record first)
+  (let* ((artist (emus-record-artist record))
+         (album (emus-record-album record))
+         (title (emus-record-title record))
+         (help-str (format "mouse-1, RET: Play '%.30s' (%.20s)" title artist)))
+    (when (or prev-record first)
+      (unless (equal (emus-record-artist prev-record) artist)
+        (insert-text-button
+         (propertize artist 'face 'emus-artist)
+         'action #'emus-click-record
+         'follow-link t
+         'help-echo help-str
+         'emus-record record)
+        (insert (propertize "\n" 'face 'emus-artist)))
+      (unless (equal (emus-record-album prev-record) album)
+        (insert-text-button
+         (propertize (concat "  " album) 'face 'emus-album)
+         'action #'emus-click-record
+         'follow-link t
+         'help-echo help-str
+         'emus-record record)
+        (insert (propertize "\n" 'face 'emus-album))))
+    (emus-set-record-browser-pos record (point))
+    (let ((is-current (equal record emus-current-record)))
+      (insert-text-button
+       (concat
+        (if is-current
+            (propertize
+             (pcase emus-state
+               ('playing "->")
+               ('paused "-)")
+               ('stopped "-]"))
+             'face 'emus-cursor)
+          (propertize "  " 'face 'default))
+        (propertize (format "   %s" title)
+                    'face (if is-current
+                              'emus-track-current
+                            'emus-track)))
+       'action #'emus-click-record
+       'follow-link t
+       'help-echo help-str
+       'emus-record record)
+      (insert (propertize "\n"
+                          'face (if is-current
+                                    'emus-track-current
+                                  'emus-track))))))
+
+(defun emus-update-record (record)
+  (let ((record-pos (emus-record-browser-pos record)))
+    (when (and (get-buffer "*emus*")
+               (emus-record-browser-pos record))
+      (with-current-buffer "*emus*"
+        (let ((inhibit-read-only t)
+              (old-point (point)))
+            (goto-char record-pos)
+            (search-forward "\n")
+            (delete-region record-pos (point))
+            (goto-char record-pos)
+            (emus-insert-record record)
+            (goto-char old-point))))))
 
 (defun emus-render-records ()
   (with-current-buffer "*emus*"
-    (let ((inhibit-read-only t))
-      (save-excursion
-        (erase-buffer)
-        (goto-char (point-min))
+    (let ((inhibit-read-only t)
+          (old-pos (point)))
+      (erase-buffer)
+      (goto-char (point-min))
+      (let ((prev-record nil))
         (dolist (record emus-records)
-          (emus-render-record record))))))
+          (emus-insert-record record prev-record (not prev-record))
+          (setq prev-record record)))
+      (goto-char old-pos))))
 
 (defun emus-click-record (button)
   (emus-play-record (button-get button 'emus-record)))
 
+(defun emus-centre-current ()
+  (interactive)
+  (when (get-buffer "*emus*")
+    (switch-to-buffer "*emus*")
+    (when emus-current-record
+      (goto-char (emus-record-browser-pos emus-current-record))
+      (recenter))))
+
 (defun emus-browse ()
   "Switch to *emus* audio library browser."
   (interactive)
   (switch-to-buffer "*emus*")
-  (emus-mode)
+  (emus-browser-mode)
   (emus-volume emus-current-volume)
   (if emus-records
       (emus-render-records)
-    (emus-update-records #'emus-render-records)))
+    (emus-update-records)))
 
-(defvar emus-mode-map
+(defvar emus-browser-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map (kbd "SPC") 'emus-playpause)
     (define-key map (kbd "o") 'emus-stop)
     (define-key map (kbd "+") 'emus-volume-up)
     (define-key map (kbd "=") 'emus-volume-up)
     (define-key map (kbd "-") 'emus-volume-down)
+    (define-key map (kbd "R") 'emus-update-records)
+    (define-key map (kbd "n") 'emus-play-next)
+    (define-key map (kbd "p") 'emus-play-prev)
+    (define-key map (kbd "c") 'emus-centre-current)
     (when (fboundp 'evil-define-key*)
       (evil-define-key* 'motion map
         (kbd "SPC") 'emus-playpause
         (kbd "o") 'emus-stop
         (kbd "+") 'emus-volume-up
         (kbd "=") 'emus-volume-up
-        (kbd "-") 'emus-volume-down))
+        (kbd "-") 'emus-volume-down
+        (kbd "R") 'emus-update-records
+        (kbd "n") 'emus-play-next
+        (kbd "p") 'emus-play-prev
+        (kbd "c") 'emus-centre-current))
     map)
   "Keymap for emus.")
 
-(define-derived-mode emus-mode special-mode "Emus"
+(define-derived-mode emus-browser-mode special-mode "emus-browser"
   "Major mode for EMUS music player.")
 
 (when (fboundp 'evil-set-initial-state)
-  (evil-set-initial-state 'emus-mode 'motion))
+  (evil-set-initial-state 'emus-browser-mode 'motion))
 
 ;;; Debugging