aboutsummaryrefslogtreecommitdiff
path: root/modules/music-config.el
diff options
context:
space:
mode:
Diffstat (limited to 'modules/music-config.el')
-rw-r--r--modules/music-config.el566
1 files changed, 320 insertions, 246 deletions
diff --git a/modules/music-config.el b/modules/music-config.el
index 7c3af0e13..d16e2bb2f 100644
--- a/modules/music-config.el
+++ b/modules/music-config.el
@@ -5,90 +5,18 @@
;; Layer: 4 (Optional).
;; Category: O/D/P/S.
;; Load shape: eager.
-;; Eager reason: none; optional music workflow that registers a music keymap, a
-;; command-loaded deferral candidate. EMMS hooks should run only after EMMS.
-;; Top-level side effects: defines a music keymap under cj/custom-keymap, one
-;; global key, package config.
+;; Eager reason: none; optional music workflow that registers a music keymap.
+;; Top-level side effects: defines C-; m map, one global key, package config.
;; Runtime requires: subr-x, user-constants, keybindings.
-;; Direct test load: yes (requires keybindings explicitly).
+;; Direct test load: yes.
;;
-;; Music management in Emacs via EMMS with MPV backend.
-;; Focus: simple, modular helpers; consistent error handling; streamlined UX.
-;;
-;; Highlights:
-;; - Fuzzy add: select files/dirs; dirs have trailing /; case-insensitive; stable order
-;; - Recursive directory add
-;; - Dired/Dirvish integration (add selection)
-;; - M3U playlist save/load/edit/reload
-;; - Radio station M3U creation (streaming URLs supported)
-;; - Playlist window toggling
-;; - Consume mode (remove tracks after playback)
-;; - MPV as player (no daemon required)
-;;
-;; Keybindings (playlist-mode-map):
-;;
-;; Aligned with ncmpcpp defaults where possible (83% match).
-;; Additional EMMS-specific bindings for features ncmpcpp lacks.
-;;
-;; Key Action ncmpcpp default Match
-;; ─── ────── ─────────────── ─────
-;; Playback
-;; SPC pause add_item *
-;; s stop stop ✓
-;; > / n next track next ✓
-;; < / P previous track previous ✓
-;; p play selected (enter) ✓
-;; f seek forward seek_forward ✓
-;; b seek backward seek_backward ✓
-;;
-;; Toggles
-;; r repeat playlist toggle_repeat ✓
-;; t repeat track (none) +
-;; z random toggle_random ✓
-;; x consume toggle_crossfade *
-;; Z shuffle shuffle ✓
-;;
-;; Volume
-;; + / = volume up volume_up ✓
-;; - volume down volume_down ✓
-;;
-;; Info
-;; i song info show_song_info ✓
-;; o jump to playing jump_to_playing ✓
-;;
-;; Playlist management
-;; a add music (fuzzy) add_selected ✓
-;; c / C clear playlist clear_playlist ✓
-;; S save playlist (none) +
-;; L load playlist (none) +
-;; E edit playlist M3U (none) +
-;; g reload playlist (none) +
-;; A append track to M3U (none) +
-;; q quit/bury quit ✓
-;;
-;; Track reordering
-;; S-up move track up (shift-up) ✓
-;; S-down move track down (shift-down) ✓
-;; C-up move track up (alias) (none) +
-;; C-down move track down (alias) (none) +
-;;
-;; Other
-;; R create radio station (none) +
-;;
-;; Legend: ✓ = matches ncmpcpp default
-;; * = intentional divergence (see below)
-;; + = EMMS-only feature
-;;
-;; Intentional divergences from ncmpcpp defaults:
-;;
-;; SPC/p swap: ncmpcpp defaults p=pause, SPC=add_item_to_playlist.
-;; This config uses SPC=pause (more natural in Emacs) and p=play
-;; selected track. Pause via SPC is a common media player convention.
-;;
-;; x=consume vs crossfade: ncmpcpp's crossfade is an mpd daemon
-;; feature. EMMS uses mpv directly, so consume mode (remove tracks
-;; after playback) is more useful here.
+;; EMMS setup using an mpv subprocess player, M3U playlist helpers, fuzzy
+;; file/directory adds, Dired/Dirvish integration, radio-station creation, and
+;; playlist window toggling.
;;
+;; The playlist keymap intentionally follows ncmpcpp where it maps cleanly, with
+;; EMMS-specific additions for M3U editing and consume mode.
+
;;; Code:
(require 'subr-x)
@@ -103,6 +31,50 @@
;; orderless never see the binding (the lexical-binding foreign-special-var trap).
(defvar orderless-smart-case)
(defvar emms-source-playlist-ask-before-overwrite)
+(defvar emms-playlist-buffer-p)
+(defvar emms-playlist-buffer)
+(defvar emms-random-playlist)
+(defvar emms-playlist-selected-marker)
+(defvar emms-source-file-default-directory)
+(defvar emms-player-playing-p)
+(defvar emms-player-paused-p)
+(defvar emms-playlist-mode-map)
+(defvar dirvish-mode-map)
+
+;; Playlist-header faces. Defined here so the `cj/music--header-text'
+;; references are valid (an undefined face spams "Invalid face reference" on
+;; every render). Appearance inherits themed base faces so the active theme
+;; owns the colors -- the literal values were dropped in the route-colors pass.
+(defface cj/music-header-face '((t :inherit shadow))
+ "Playlist-header field labels (Playlist, Current, Mode, Keys).")
+(defface cj/music-header-value-face '((t :inherit default))
+ "Playlist-header field values.")
+(defface cj/music-mode-on-face '((t :inherit warning))
+ "Active mode indicator in the playlist header.")
+(defface cj/music-mode-off-face '((t :inherit shadow))
+ "Inactive mode indicator in the playlist header.")
+(defface cj/music-keyhint-face '((t :inherit shadow))
+ "Key hints in the playlist header.")
+
+;; Foreign functions used lazily after their packages load.
+(declare-function emms-playlist-mode "emms-playlist-mode")
+(declare-function emms-playlist-track-at "emms-playlist-mode")
+(declare-function emms-playlist-mode-kill-track "emms-playlist-mode")
+(declare-function emms-track-name "emms")
+(declare-function emms-track-type "emms")
+(declare-function emms-track-get "emms")
+(declare-function emms-track-simple-description "emms")
+(declare-function emms-playlist-current-selected-track "emms")
+(declare-function emms-playlist-select "emms")
+(declare-function emms-playlist-clear "emms")
+(declare-function emms-playlist-save "emms-source-playlist")
+(declare-function emms-start "emms")
+(declare-function emms-random "emms")
+(declare-function emms-next "emms")
+(declare-function emms-previous "emms")
+(declare-function dired-get-marked-files "dired")
+(declare-function dired-get-file-for-visit "dired")
+(declare-function face-remap-remove-relative "face-remap")
;;; Settings (no Customize)
@@ -115,9 +87,98 @@
(defvar cj/music-file-extensions '("aac" "flac" "m4a" "mp3" "ogg" "opus" "wav")
"List of valid music file extensions.")
+(defvar cj/music-seek-seconds 5
+ "Seconds to move when seeking forward or backward in the current track.")
+
(defvar cj/music-playlist-buffer-name "*EMMS-Playlist*"
"Name of the EMMS playlist buffer used by this configuration.")
+;;; Subprocess mpv player (reliable playback)
+
+;; The IPC player (emms-player-mpv) drives mpv over a socket -- start mpv idle,
+;; connect, send loadfile. That handshake was leaving mpv loaded but never
+;; streaming, so playback silently failed. Driving mpv with the track as a
+;; direct argument -- the invocation that plays every time -- is the reliable
+;; path. --no-config isolates this mpv from the interactive/video mpv setup so
+;; the two cannot interfere. Pause is in place via process signals; in-track
+;; seek is not available with a subprocess player (the trade for reliability).
+
+(declare-function emms-player "emms")
+(declare-function emms-player-set "emms")
+(declare-function emms-player-simple-start "emms-player-simple")
+(declare-function emms-player-simple-stop "emms-player-simple")
+(defvar emms-player-simple-process-name)
+(defvar emms-player-cj/music-mpv)
+
+(defvar cj/music--mpv-regex
+ (concat "\\(?:\\." (regexp-opt cj/music-file-extensions) "\\'\\)"
+ "\\|\\`\\(?:https?\\|mms\\)://")
+ "Track names the subprocess mpv player handles: music files or stream URLs.")
+
+(defvar cj/music--mpv-socket
+ (expand-file-name "emms/mpv-control.sock" user-emacs-directory)
+ "IPC control socket for the subprocess mpv player.
+mpv opens it per playback via --input-ipc-server. It does NOT affect startup:
+mpv still plays the track passed as a direct argument, so the reliable start is
+unchanged. The socket only carries control commands (seek) to the already
+playing process, which is where the old idle + loadfile handshake failed.")
+
+(defun cj/music--mpv-start (track)
+ "Play TRACK by running mpv with the track name as a direct argument."
+ (emms-player-simple-start (emms-track-name track)
+ 'emms-player-cj/music-mpv
+ "mpv"
+ (list "--no-video" "--no-config" "--really-quiet"
+ (concat "--input-ipc-server=" cj/music--mpv-socket))))
+
+(defun cj/music--mpv-stop ()
+ "Stop the mpv subprocess."
+ (emms-player-simple-stop))
+
+(defun cj/music--mpv-playable-p (track)
+ "Return non-nil if the subprocess mpv player can play TRACK."
+ (and (executable-find "mpv")
+ (memq (emms-track-type track) '(file url))
+ (string-match cj/music--mpv-regex (emms-track-name track))))
+
+(defun cj/music--mpv-pause ()
+ "Pause the mpv subprocess in place by stopping it (SIGSTOP)."
+ (let ((proc (get-process emms-player-simple-process-name)))
+ (when (and proc (process-live-p proc))
+ (signal-process proc 'SIGSTOP))))
+
+(defun cj/music--mpv-resume ()
+ "Resume the paused mpv subprocess (SIGCONT)."
+ (let ((proc (get-process emms-player-simple-process-name)))
+ (when (and proc (process-live-p proc))
+ (signal-process proc 'SIGCONT))))
+
+(defun cj/music--mpv-command (json)
+ "Send JSON (a one-line mpv IPC command) to the control socket.
+A no-op when nothing is playing or the socket is gone, so it never errors."
+ (when (file-exists-p cj/music--mpv-socket)
+ (ignore-errors
+ (let ((proc (make-network-process :name "cj-music-mpv-cmd"
+ :family 'local
+ :service cj/music--mpv-socket
+ :noquery t)))
+ (unwind-protect
+ (progn (process-send-string proc (concat json "\n"))
+ (accept-process-output proc 0.1))
+ (delete-process proc))))))
+
+(defun cj/music-seek-forward ()
+ "Seek `cj/music-seek-seconds' seconds forward in the current track."
+ (interactive)
+ (cj/music--mpv-command
+ (format "{\"command\": [\"seek\", %d, \"relative\"]}" cj/music-seek-seconds)))
+
+(defun cj/music-seek-backward ()
+ "Seek `cj/music-seek-seconds' seconds backward in the current track."
+ (interactive)
+ (cj/music--mpv-command
+ (format "{\"command\": [\"seek\", %d, \"relative\"]}" (- cj/music-seek-seconds))))
+
;;; Buffer-local state
(defvar-local cj/music-playlist-file nil
@@ -158,14 +219,24 @@ Directories are suffixed with /; files are plain. Hidden dirs/files skipped."
(sort acc #'string-lessp)))
(defun cj/music--completion-table (candidates)
- "Completion table for CANDIDATES preserving order and case-insensitive match."
- (lambda (string pred action)
- (if (eq action 'metadata)
- '(metadata
- (display-sort-function . identity)
- (cycle-sort-function . identity)
- (completion-ignore-case . t))
- (complete-with-action action candidates string pred))))
+ "Completion table for CANDIDATES preserving order and case-insensitive match.
+Tags the `cj-music-file' category and annotates each candidate (a path relative
+to `cj/music-root', with a trailing slash for directories) with its size and
+modification date so marginalia can show them."
+ (let ((annotate (cj/completion-file-annotator
+ (lambda (c)
+ (expand-file-name
+ (if (string-suffix-p "/" c) (substring c 0 -1) c)
+ cj/music-root)))))
+ (lambda (string pred action)
+ (if (eq action 'metadata)
+ `(metadata
+ (category . cj-music-file)
+ (annotation-function . ,annotate)
+ (display-sort-function . identity)
+ (cycle-sort-function . identity)
+ (completion-ignore-case . t))
+ (complete-with-action action candidates string pred)))))
(defun cj/music--ensure-playlist-buffer ()
"Ensure EMMS playlist buffer exists and is in playlist mode. Return buffer."
@@ -619,26 +690,26 @@ Initializes EMMS if needed."
;;; Dired/Dirvish integration
-(with-eval-after-load 'dirvish
- (defun cj/music-add-dired-selection ()
- "Add selected files/dirs in Dired/Dirvish to the EMMS playlist.
+(defun cj/music-add-dired-selection ()
+ "Add selected files/dirs in Dired/Dirvish to the EMMS playlist.
Dirs added recursively."
- (interactive)
- (unless (derived-mode-p 'dired-mode)
- (user-error "This command must be run in a Dired buffer"))
- (cj/music--ensure-playlist-buffer)
- (let ((files (if (use-region-p)
- (dired-get-marked-files)
- (list (dired-get-file-for-visit)))))
- (when (null files)
- (user-error "No files selected"))
- (dolist (file files)
- (cond
- ((file-directory-p file) (cj/music-add-directory-recursive file))
- ((cj/music--valid-file-p file) (emms-add-file file))
- (t (message "Skipping non-music file: %s" file))))
- (message "Added %d item(s) to playlist" (length files))))
+ (interactive)
+ (unless (derived-mode-p 'dired-mode)
+ (user-error "This command must be run in a Dired buffer"))
+ (cj/music--ensure-playlist-buffer)
+ (let ((files (if (use-region-p)
+ (dired-get-marked-files)
+ (list (dired-get-file-for-visit)))))
+ (when (null files)
+ (user-error "No files selected"))
+ (dolist (file files)
+ (cond
+ ((file-directory-p file) (cj/music-add-directory-recursive file))
+ ((cj/music--valid-file-p file) (emms-add-file file))
+ (t (message "Skipping non-music file: %s" file))))
+ (message "Added %d item(s) to playlist" (length files))))
+(with-eval-after-load 'dirvish
(keymap-set dirvish-mode-map "+" #'cj/music-add-dired-selection))
;;; EMMS setup and keybindings
@@ -680,6 +751,130 @@ Dirs added recursively."
"C-; m z" "random"
"C-; m x" "consume"))
+;;; Playlist display helpers
+;;
+;; Defined at top level (not inside the `emms' use-package `:config') so the
+;; byte-compiler sees them; they touch EMMS only at call time, after load.
+
+(defun cj/music--after-playlist-clear (&rest _)
+ "Forget the associated M3U file after the playlist is cleared."
+ (when-let ((buf (get-buffer cj/music-playlist-buffer-name)))
+ (with-current-buffer buf
+ (setq cj/music-playlist-file nil))))
+
+(defun cj/music--format-duration (seconds)
+ "Convert SECONDS to a \"M:SS\" string."
+ (when (and seconds (numberp seconds) (> seconds 0))
+ (format "%d:%02d" (/ seconds 60) (mod seconds 60))))
+
+(defun cj/music--track-description (track)
+ "Return a human-readable description of TRACK.
+For tagged tracks: \"Artist - Title [M:SS]\".
+For file tracks without tags: filename without path or extension.
+For URL tracks: decoded URL."
+ (let ((type (emms-track-type track))
+ (title (emms-track-get track 'info-title))
+ (artist (emms-track-get track 'info-artist))
+ (duration (emms-track-get track 'info-playing-time))
+ (name (emms-track-name track)))
+ (cond
+ ;; Tagged track with title
+ (title
+ (let ((dur-str (cj/music--format-duration duration))
+ (parts '()))
+ (when artist (push artist parts))
+ (push title parts)
+ (let ((desc (string-join (nreverse parts) " - ")))
+ (if dur-str (format "%s [%s]" desc dur-str) desc))))
+ ;; File without tags — show clean filename
+ ((eq type 'file)
+ (file-name-sans-extension (file-name-nondirectory name)))
+ ;; URL — decode percent-encoded characters
+ ((eq type 'url)
+ (decode-coding-string (url-unhex-string name) 'utf-8))
+ ;; Fallback
+ (t (emms-track-simple-description track)))))
+
+;; Multi-line header overlay
+(defvar-local cj/music--header-overlay nil
+ "Overlay displaying the playlist header.")
+
+(defun cj/music--header-text ()
+ "Build a multi-line header string for the playlist buffer overlay."
+ (let* ((pl-name (if cj/music-playlist-file
+ (file-name-sans-extension
+ (file-name-nondirectory cj/music-playlist-file))
+ "Untitled"))
+ (track-count (count-lines (point-min) (point-max)))
+ (now-playing (cond
+ ((not emms-player-playing-p) "Stopped")
+ (emms-player-paused-p "Paused")
+ (t (let ((track (emms-playlist-current-selected-track)))
+ (if track
+ (cj/music--track-description track)
+ "Playing")))))
+ (mode-indicator
+ (lambda (key label active)
+ (let ((face (if active 'cj/music-mode-on-face 'cj/music-mode-off-face)))
+ (propertize (format "[%s] %s" key label) 'face face)))))
+ (concat
+ (propertize "Playlist" 'face 'cj/music-header-face)
+ (propertize " : " 'face 'cj/music-header-face)
+ (propertize (format "%s (%d)" pl-name track-count) 'face 'cj/music-header-value-face)
+ "\n"
+ (propertize "Current " 'face 'cj/music-header-face)
+ (propertize " : " 'face 'cj/music-header-face)
+ (propertize now-playing 'face 'cj/music-header-value-face)
+ "\n"
+ (propertize "Mode " 'face 'cj/music-header-face)
+ (propertize " : " 'face 'cj/music-header-face)
+ (funcall mode-indicator "r" "repeat" (bound-and-true-p emms-repeat-playlist))
+ " "
+ (funcall mode-indicator "t" "single" (bound-and-true-p emms-repeat-track))
+ " "
+ (funcall mode-indicator "z" "random" (bound-and-true-p emms-random-playlist))
+ " "
+ (funcall mode-indicator "x" "consume" cj/music-consume-mode)
+ "\n"
+ (propertize "Keys " 'face 'cj/music-header-face)
+ (propertize " : " 'face 'cj/music-header-face)
+ (propertize "a:add c:clear L:load S:save SPC:pause <>:skip ↑↓:move C-↑↓:reorder q:dismiss"
+ 'face 'cj/music-keyhint-face)
+ "\n\n")))
+
+(defun cj/music--update-header ()
+ "Insert or update the multi-line header overlay in the playlist buffer."
+ (when-let ((buf (get-buffer cj/music-playlist-buffer-name)))
+ (with-current-buffer buf
+ (unless cj/music--header-overlay
+ (setq cj/music--header-overlay (make-overlay (point-min) (point-min)))
+ (overlay-put cj/music--header-overlay 'priority 100))
+ (move-overlay cj/music--header-overlay (point-min) (point-min))
+ (overlay-put cj/music--header-overlay 'before-string
+ (cj/music--header-text)))))
+
+(defvar-local cj/music--bg-remap-cookie nil
+ "Cookie for the active-window background face remapping.")
+
+(defun cj/music--update-active-bg (&rest _)
+ "Toggle playlist buffer background based on whether its window is selected."
+ (when-let ((buf (get-buffer cj/music-playlist-buffer-name)))
+ (with-current-buffer buf
+ (let ((active (eq buf (window-buffer (selected-window)))))
+ (cond
+ ((and active (not cj/music--bg-remap-cookie))
+ (setq cj/music--bg-remap-cookie
+ (face-remap-add-relative 'default)))
+ ((and (not active) cj/music--bg-remap-cookie)
+ (face-remap-remove-relative cj/music--bg-remap-cookie)
+ (setq cj/music--bg-remap-cookie nil)))))))
+
+(defun cj/music--setup-playlist-display ()
+ "Set up header overlay and focus tracking in the playlist buffer."
+ (setq header-line-format nil)
+ (cj/music--update-header)
+ (add-hook 'window-selection-change-functions #'cj/music--update-active-bg nil t))
+
(use-package emms
:defer t
:init
@@ -688,7 +883,7 @@ Dirs added recursively."
:commands (emms-mode-line-mode)
:config
(require 'emms-setup)
- (require 'emms-player-mpv)
+ (require 'emms-player-simple)
(require 'emms-playlist-mode)
(require 'emms-source-file)
(require 'emms-source-playlist)
@@ -697,155 +892,31 @@ Dirs added recursively."
(setq emms-source-file-default-directory cj/music-root)
(setq emms-playlist-default-major-mode 'emms-playlist-mode)
- ;; Use MPV as player - MUST be set before emms-all
- (setq emms-player-list '(emms-player-mpv))
+ ;; Use the reliable subprocess mpv player (built above) - MUST be set before emms-all
+ (setq emms-player-cj/music-mpv
+ (emms-player #'cj/music--mpv-start #'cj/music--mpv-stop
+ #'cj/music--mpv-playable-p))
+ (emms-player-set emms-player-cj/music-mpv 'pause #'cj/music--mpv-pause)
+ (emms-player-set emms-player-cj/music-mpv 'resume #'cj/music--mpv-resume)
+ (setq emms-player-list '(emms-player-cj/music-mpv))
;; Now initialize EMMS
(emms-all)
;; Disable modeline display (keep modeline clean)
- (emms-playing-time-disable-display)
+ (emms-playing-time-display-mode -1)
(emms-mode-line-mode -1)
- ;; MPV configuration
- ;; MPV supports both local files and stream URLs
- (setq emms-player-mpv-parameters
- '("--quiet" "--no-video" "--audio-display=no"))
-
- ;; Update supported file types for mpv player
- (setq emms-player-mpv-regexp
- (concat "\\(?:\\`\\(?:https?\\|mms\\)://\\)\\|\\(?:\\."
- (regexp-opt cj/music-file-extensions)
- "\\'\\)"))
-
- ;; Keep cj/music-playlist-file in sync if playlist is cleared
- (defun cj/music--after-playlist-clear (&rest _)
- (when-let ((buf (get-buffer cj/music-playlist-buffer-name)))
- (with-current-buffer buf
- (setq cj/music-playlist-file nil))))
-
- ;; Ensure we don't stack duplicate advice on reload
+ ;; Keep cj/music-playlist-file in sync if playlist is cleared.
+ ;; Ensure we don't stack duplicate advice on reload.
(advice-remove 'emms-playlist-clear #'cj/music--after-playlist-clear)
(advice-add 'emms-playlist-clear :after #'cj/music--after-playlist-clear)
;;; Playlist display
;; Track description: show "Artist - Title [M:SS]" instead of file paths
- (defun cj/music--format-duration (seconds)
- "Convert SECONDS to a \"M:SS\" string."
- (when (and seconds (numberp seconds) (> seconds 0))
- (format "%d:%02d" (/ seconds 60) (mod seconds 60))))
-
- (defun cj/music--track-description (track)
- "Return a human-readable description of TRACK.
-For tagged tracks: \"Artist - Title [M:SS]\".
-For file tracks without tags: filename without path or extension.
-For URL tracks: decoded URL."
- (let ((type (emms-track-type track))
- (title (emms-track-get track 'info-title))
- (artist (emms-track-get track 'info-artist))
- (duration (emms-track-get track 'info-playing-time))
- (name (emms-track-name track)))
- (cond
- ;; Tagged track with title
- (title
- (let ((dur-str (cj/music--format-duration duration))
- (parts '()))
- (when artist (push artist parts))
- (push title parts)
- (let ((desc (string-join (nreverse parts) " - ")))
- (if dur-str (format "%s [%s]" desc dur-str) desc))))
- ;; File without tags — show clean filename
- ((eq type 'file)
- (file-name-sans-extension (file-name-nondirectory name)))
- ;; URL — decode percent-encoded characters
- ((eq type 'url)
- (decode-coding-string (url-unhex-string name) 'utf-8))
- ;; Fallback
- (t (emms-track-simple-description track)))))
-
(setq emms-track-description-function #'cj/music--track-description)
- ;; Multi-line header overlay
- (defvar-local cj/music--header-overlay nil
- "Overlay displaying the playlist header.")
-
- (defun cj/music--header-text ()
- "Build a multi-line header string for the playlist buffer overlay."
- (let* ((pl-name (if cj/music-playlist-file
- (file-name-sans-extension
- (file-name-nondirectory cj/music-playlist-file))
- "Untitled"))
- (track-count (count-lines (point-min) (point-max)))
- (now-playing (cond
- ((not emms-player-playing-p) "Stopped")
- (emms-player-paused-p "Paused")
- (t (let ((track (emms-playlist-current-selected-track)))
- (if track
- (cj/music--track-description track)
- "Playing")))))
- (mode-indicator
- (lambda (key label active)
- (let ((face (if active 'cj/music-mode-on-face 'cj/music-mode-off-face)))
- (propertize (format "[%s] %s" key label) 'face face)))))
- (concat
- (propertize "Playlist" 'face 'cj/music-header-face)
- (propertize " : " 'face 'cj/music-header-face)
- (propertize (format "%s (%d)" pl-name track-count) 'face 'cj/music-header-value-face)
- "\n"
- (propertize "Current " 'face 'cj/music-header-face)
- (propertize " : " 'face 'cj/music-header-face)
- (propertize now-playing 'face 'cj/music-header-value-face)
- "\n"
- (propertize "Mode " 'face 'cj/music-header-face)
- (propertize " : " 'face 'cj/music-header-face)
- (funcall mode-indicator "r" "repeat" (bound-and-true-p emms-repeat-playlist))
- " "
- (funcall mode-indicator "t" "single" (bound-and-true-p emms-repeat-track))
- " "
- (funcall mode-indicator "z" "random" (bound-and-true-p emms-random-playlist))
- " "
- (funcall mode-indicator "x" "consume" cj/music-consume-mode)
- "\n"
- (propertize "Keys " 'face 'cj/music-header-face)
- (propertize " : " 'face 'cj/music-header-face)
- (propertize "a:add c:clear L:load S:save SPC:pause <>:skip ↑↓:move C-↑↓:reorder q:dismiss"
- 'face 'cj/music-keyhint-face)
- "\n\n")))
-
- (defun cj/music--update-header ()
- "Insert or update the multi-line header overlay in the playlist buffer."
- (when-let ((buf (get-buffer cj/music-playlist-buffer-name)))
- (with-current-buffer buf
- (unless cj/music--header-overlay
- (setq cj/music--header-overlay (make-overlay (point-min) (point-min)))
- (overlay-put cj/music--header-overlay 'priority 100))
- (move-overlay cj/music--header-overlay (point-min) (point-min))
- (overlay-put cj/music--header-overlay 'before-string
- (cj/music--header-text)))))
-
- (defvar-local cj/music--bg-remap-cookie nil
- "Cookie for the active-window background face remapping.")
-
- (defun cj/music--update-active-bg (&rest _)
- "Toggle playlist buffer background based on whether its window is selected."
- (when-let ((buf (get-buffer cj/music-playlist-buffer-name)))
- (with-current-buffer buf
- (let ((active (eq buf (window-buffer (selected-window)))))
- (cond
- ((and active (not cj/music--bg-remap-cookie))
- (setq cj/music--bg-remap-cookie
- (face-remap-add-relative 'default :background "#1d1b19")))
- ((and (not active) cj/music--bg-remap-cookie)
- (face-remap-remove-relative cj/music--bg-remap-cookie)
- (setq cj/music--bg-remap-cookie nil)))))))
-
- (defun cj/music--setup-playlist-display ()
- "Set up header overlay and focus tracking in the playlist buffer."
- (setq header-line-format nil)
- (cj/music--update-header)
- (add-hook 'window-selection-change-functions #'cj/music--update-active-bg nil t))
-
(add-hook 'emms-playlist-mode-hook #'cj/music--setup-playlist-display)
(add-hook 'emms-player-started-hook #'cj/music--record-random-history)
(add-hook 'emms-player-started-hook #'cj/music--update-header)
@@ -871,8 +942,8 @@ For URL tracks: decoded URL."
(">" . cj/music-next)
("P" . cj/music-previous)
("<" . cj/music-previous)
- ("f" . emms-seek-forward)
- ("b" . emms-seek-backward)
+ ("f" . cj/music-seek-forward)
+ ("b" . cj/music-seek-backward)
("q" . emms-playlist-mode-bury-buffer)
("a" . cj/music-fuzzy-select-and-add)
;; Toggles (aligned with ncmpcpp)
@@ -897,8 +968,6 @@ For URL tracks: decoded URL."
("S-<down>" . emms-playlist-mode-shift-track-down)
("C-<up>" . emms-playlist-mode-shift-track-up)
("C-<down>" . emms-playlist-mode-shift-track-down)
- ;; Radio
- ("R" . cj/music-create-radio-station)
;; Volume
("+" . emms-volume-raise)
("=" . emms-volume-raise)
@@ -927,5 +996,10 @@ For URL tracks: decoded URL."
(insert content))
(message "Created radio station: %s" (file-name-nondirectory file))))
+;; Bound here rather than in the emms `:bind' so use-package does not emit a
+;; redundant autoload that collides with this same-file definition.
+(with-eval-after-load 'emms
+ (keymap-set emms-playlist-mode-map "R" #'cj/music-create-radio-station))
+
(provide 'music-config)
;;; music-config.el ends here