diff options
Diffstat (limited to 'modules/music-config.el')
| -rw-r--r-- | modules/music-config.el | 583 |
1 files changed, 338 insertions, 245 deletions
diff --git a/modules/music-config.el b/modules/music-config.el index be836429b..86f6eb130 100644 --- a/modules/music-config.el +++ b/modules/music-config.el @@ -5,98 +5,62 @@ ;; 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) (require 'user-constants) (require 'keybindings) ;; provides cj/custom-keymap +(require 'cj-window-geometry-lib) ;; cj/preferred-dock-direction (F10 dock side) (require 'cj-window-toggle-lib) ;; side-window size memory (F10 toggle) (require 'system-lib) ;; cj/confirm-strong (overwrite confirms) +;; Declare these foreign package vars special so `let'-binding them below +;; compiles as a dynamic bind, not a dead lexical local -- otherwise emms / +;; 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) + +;; 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) (defvar cj/music-root music-dir @@ -108,9 +72,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 @@ -517,14 +570,38 @@ Intended for use on `emms-player-finished-hook'." (defvar cj/music-playlist-window-height 0.3 "Default fraction of frame height for the F10 music playlist side window. -Used until the playlist is resized and toggled off this session; after that, -the toggled-off height is remembered in `cj/--music-playlist-height'.") +Used when the playlist docks at the bottom and hasn't been resized and +toggled off this session; after that, the toggled-off height is remembered +in `cj/--music-playlist-height'.") + +(defvar cj/music-playlist-window-width 0.4 + "Default fraction of frame width for the F10 music playlist side window. +Used when the playlist docks as a right-side column (see +`cj/--music-playlist-side') and hasn't been resized this session; after +that the toggled-off width is remembered in `cj/--music-playlist-width'.") (defvar cj/--music-playlist-height nil - "Last height fraction the playlist side window was toggled off at. + "Last height fraction the playlist was toggled off at while docked bottom. nil means fall back to `cj/music-playlist-window-height'. In-memory only -- resets each Emacs session.") +(defvar cj/--music-playlist-width nil + "Last width fraction the playlist was toggled off at while docked right. +nil means fall back to `cj/music-playlist-window-width'. In-memory only -- +resets each Emacs session.") + +(defun cj/--music-playlist-side () + "Return the side the F10 playlist should dock on: `right' or `bottom'. +Docks as a right-side column only when a side-by-side split would leave +both panes at least `cj/window-dock-min-columns' wide (the playlist's +share is `cj/music-playlist-window-width'); otherwise docks at the bottom. +See `cj/preferred-dock-direction'." + (if (eq (cj/preferred-dock-direction (frame-width) + cj/music-playlist-window-width) + 'right) + 'right + 'bottom)) + (defun cj/music-playlist-toggle () "Toggle the EMMS playlist buffer in a bottom side window. The window opens at `cj/music-playlist-window-height'; if it has been @@ -535,15 +612,28 @@ resized and toggled off this session, it reopens at that remembered height." (win (and buffer (get-buffer-window buffer)))) (if win (progn - (cj/side-window-capture-size win 'bottom 'cj/--music-playlist-height) + ;; Capture the resized size into the var matching the window's + ;; actual side, so width and height memories stay independent. + ;; Guard the parameter lookup: a dead or non-window WIN (the + ;; capture helpers tolerate one) must not error here. + (let ((side (if (window-live-p win) + (or (window-parameter win 'window-side) 'bottom) + 'bottom))) + (if (memq side '(left right)) + (cj/side-window-capture-size win side 'cj/--music-playlist-width) + (cj/side-window-capture-size win 'bottom 'cj/--music-playlist-height))) (delete-window win) (message "Playlist window closed")) (progn (cj/emms--setup) (setq buffer (cj/music--ensure-playlist-buffer)) - (setq win (cj/side-window-display - buffer 'bottom 'cj/--music-playlist-height - cj/music-playlist-window-height)) + (let* ((side (cj/--music-playlist-side)) + (right (eq side 'right))) + (setq win (cj/side-window-display + buffer side + (if right 'cj/--music-playlist-width 'cj/--music-playlist-height) + (if right cj/music-playlist-window-width + cj/music-playlist-window-height)))) (select-window win) (with-current-buffer buffer (if (and (fboundp 'emms-playlist-current-selected-track) @@ -575,26 +665,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 @@ -636,6 +726,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 @@ -644,7 +858,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) @@ -653,155 +867,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) @@ -827,8 +917,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) @@ -853,8 +943,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) @@ -883,5 +971,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 |
