;;; naviel.el --- Navidrome music player client for Emacs -*- lexical-binding: t -*- ;; Author: CdatGoose ;; Version: 0.2 ;; Package-Requires: ((emacs "28.1")) ;; Keywords: multimedia, music ;;; Commentary: ;; A Navidrome client for Emacs using the Subsonic API. ;; Plays music via mpv (must be installed on PATH). ;; This project is glued together by sheer hope, I don't know elisp and probably never will. ;; Quick setup: ;; 1. ~/.authinfo: machine your-host login USER password PASS ;; 2. (setq naviel-url "http://host:4533") ;; 3. M-x naviel ;; Code: (require 'url) (require 'url-util) (require 'xml) (require 'auth-source) (require 'cl-lib) (require 'json) (require 'seq) ;;; Customisation (defgroup naviel nil "Naviel music client." :group 'multimedia :prefix "naviel-") (defcustom naviel-url "http://localhost:4533" "Base URL of the Navidrome server (no trailing slash)." :type 'string :group 'naviel) (defcustom naviel-username nil "Username. If nil, looked up from ~/.authinfo." :type '(choice (const nil) string) :group 'naviel) (defcustom naviel-password nil "Password. If nil, looked up from ~/.authinfo." :type '(choice (const nil) string) :group 'naviel) (defcustom naviel-client-name "naviel.el" "Client identifier sent with every API request." :type 'string :group 'naviel) (defcustom naviel-api-version "1.16.1" "Subsonic API version declared in requests." :type 'string :group 'naviel) (defcustom naviel-mpv-executable "mpv" "Path to the mpv executable." :type 'string :group 'naviel) (defcustom naviel-volume-step 5 "Volume increment/decrement step (0-100)." :type 'integer :group 'naviel) (defcustom naviel-seek-step 10 "Seek step in seconds for the [ / ] keys." :type 'integer :group 'naviel) (defcustom naviel-position-poll-interval 1.0 "Seconds between IPC polls for playback position." :type 'float :group 'naviel) (defcustom naviel-album-list-size 30 "Default number of albums fetched for recently-added/played views." :type 'integer :group 'naviel) (defcustom naviel-genre-song-limit 200 "Maximum songs fetched when browsing a genre." :type 'integer :group 'naviel) ;;; Faces (defface naviel-header-face '((t :inherit bold :height 1.15)) "Main header.") (defface naviel-separator-face '((t :inherit shadow)) "Separator lines.") (defface naviel-artist-face '((t :inherit font-lock-function-name-face :weight semi-bold)) "Artist names.") (defface naviel-album-face '((t :inherit font-lock-string-face)) "Album names.") (defface naviel-song-title-face '((t :inherit default)) "Song titles.") (defface naviel-song-meta-face '((t :inherit shadow)) "Song metadata.") (defface naviel-track-num-face '((t :inherit shadow)) "Track numbers.") (defface naviel-year-face '((t :inherit shadow)) "Album years.") (defface naviel-now-playing-face '((t :inherit success :weight bold)) "Now-playing text.") (defface naviel-paused-face '((t :inherit warning :weight bold)) "Paused indicator.") (defface naviel-footer-face '((t :inherit shadow :height 0.85)) "Footer hints.") (defface naviel-count-face '((t :inherit shadow)) "Item counts.") (defface naviel-breadcrumb-face '((t :inherit font-lock-comment-face)) "Breadcrumb trail.") (defface naviel-repeat-face '((t :inherit font-lock-keyword-face)) "Repeat mode.") (defface naviel-error-face '((t :inherit error)) "In-buffer errors.") (defface naviel-position-face '((t :inherit shadow)) "Playback position.") ;;; Repeat mode (defvar naviel-repeat-mode 'off "Playback repeat mode: \\='off, \\='one, or \\='album.") (defun naviel-toggle-repeat () "Cycle repeat mode: off -> one -> album -> off." (interactive) (setq naviel-repeat-mode (pcase naviel-repeat-mode ('off 'one) ('one 'album) (_ 'off))) (naviel--refresh-footer) (message "Repeat: %s" (naviel--repeat-label))) (defun naviel--repeat-label () (pcase naviel-repeat-mode ('one "one") ('album "all") (_ "off"))) ;;; Internal state (defvar naviel--process nil) (defvar naviel--ipc-process nil) (defvar naviel--ipc-buffer "") (defvar naviel--ipc-request-id 0) (defvar naviel--ipc-callbacks nil) (defvar naviel--position-timer nil) (defvar naviel--elapsed nil) (defvar naviel--duration nil) (defvar naviel--queue '()) (defvar naviel--queue-index 0) (defvar naviel--volume 80) (defvar naviel--paused nil) (defvar naviel--current-song nil) (defvar naviel--browser-stack '()) (defvar naviel--current-view nil) (defvar naviel--mode-line-timer nil) ;;; Authentication (defun naviel--credentials () (let* ((host (url-host (url-generic-parse-url naviel-url))) (user (or naviel-username (plist-get (car (auth-source-search :host host :require '(:user))) :user))) (pass (or naviel-password (let* ((entry (car (auth-source-search :host host :require '(:secret)))) (s (plist-get entry :secret))) (when s (if (functionp s) (funcall s) s)))))) (unless (and user pass) (error "naviel: no credentials for %s - set naviel-username/naviel-password or use ~/.authinfo" host)) (cons user pass))) (defun naviel--auth-params () (let* ((creds (naviel--credentials)) (salt (naviel--random-salt 10)) (token (md5 (concat (cdr creds) salt)))) `(("u" . ,(car creds)) ("t" . ,token) ("s" . ,salt) ("v" . ,naviel-api-version) ("c" . ,naviel-client-name) ("f" . "xml")))) (defun naviel--random-salt (len) (let ((chars "abcdefghijklmnopqrstuvwxyz0123456789") (r "")) (dotimes (_ len r) (setq r (concat r (string (aref chars (random (length chars))))))))) ;;; Async HTTP / API (defun naviel--build-url (endpoint params) (let* ((all (append (naviel--auth-params) params)) (q (mapconcat (lambda (p) (concat (url-hexify-string (car p)) "=" (url-hexify-string (cdr p)))) all "&"))) (format "%s/rest/%s?%s" naviel-url endpoint q))) (defun naviel--request-async (endpoint params parse-fn callback) (let ((url (naviel--build-url endpoint params))) (url-retrieve url (lambda (status) (if (plist-get status :error) (progn (ignore-errors (kill-buffer (current-buffer))) (naviel--show-error (format "Could not reach %s - is the server running?" naviel-url)) (funcall callback nil)) (condition-case err (progn (goto-char (point-min)) (re-search-forward "\r?\n\r?\n" nil t) (let* ((xml (xml-parse-region (point) (point-max))) (response (naviel--check-status xml)) (result (funcall parse-fn response))) (kill-buffer (current-buffer)) (funcall callback result))) (error (ignore-errors (kill-buffer (current-buffer))) (naviel--show-error (error-message-string err)) (funcall callback nil))))) nil t))) (defun naviel--check-status (xml) (let* ((root (car xml)) (response (if (eq (xml-node-name root) 'subsonic-response) root (car (xml-get-children root 'subsonic-response)))) (status (xml-get-attribute-or-nil response 'status))) (unless response (error "naviel: unexpected XML response structure")) (when (string= status "failed") (let* ((err (car (xml-get-children response 'error))) (code (xml-get-attribute err 'code)) (msg (xml-get-attribute err 'message))) (error "naviel API error %s: %s" code msg))) response)) (defun naviel--get-child (r tag) (car (xml-get-children r tag))) (defun naviel--attr (node attr) (xml-get-attribute node attr)) ;;; Error display (defun naviel--show-error (msg) (with-current-buffer (naviel--browser-buffer) (naviel-browser-mode) (let ((inhibit-read-only t)) (erase-buffer) (insert (propertize "\n X naviel error\n\n" 'face 'naviel-error-face)) (insert (propertize (format " %s\n\n" msg) 'face 'naviel-error-face)) (insert (propertize " Press g to retry, q to quit.\n" 'face 'naviel-footer-face)) (goto-char (point-min)))) (pop-to-buffer (naviel--browser-buffer))) ;;; Stream URL (defun naviel--stream-url (song-id) (naviel--build-url "stream" `(("id" . ,song-id)))) ;;; mpv IPC (defun naviel--ipc-socket-path () (expand-file-name "naviel-mpv.sock" temporary-file-directory)) (defun naviel--ipc-connect () (let ((sock (naviel--ipc-socket-path)) (limit (+ (float-time) 1.5))) (while (and (not (file-exists-p sock)) (< (float-time) limit)) (sleep-for 0.05)) (when (file-exists-p sock) (condition-case err (setq naviel--ipc-process (make-network-process :name "naviel-ipc" :family 'local :service sock :filter #'naviel--ipc-filter :sentinel #'naviel--ipc-sentinel :coding 'utf-8-unix) naviel--ipc-buffer "") (error (message "naviel: IPC connect failed - %s" (error-message-string err))))))) (defun naviel--ipc-filter (_proc string) (setq naviel--ipc-buffer (concat naviel--ipc-buffer string)) (let ((start 0)) (while (string-match "\n" naviel--ipc-buffer start) (let* ((end (match-end 0)) (line (substring naviel--ipc-buffer start (1- end)))) (setq start end) (naviel--ipc-dispatch line))) (setq naviel--ipc-buffer (substring naviel--ipc-buffer start)))) (defun naviel--ipc-sentinel (proc event) (when (and (string-match-p "deleted\\|closed\\|failed" event) (eq proc naviel--ipc-process)) (setq naviel--ipc-process nil))) (defun naviel--ipc-dispatch (json-line) (condition-case nil (let* ((obj (json-read-from-string json-line)) (event (cdr (assq 'event obj))) (req-id (cdr (assq 'request_id obj))) (data (cdr (assq 'data obj))) (err (cdr (assq 'error obj)))) (when req-id (let ((cb (cdr (assq req-id naviel--ipc-callbacks)))) (when cb (setq naviel--ipc-callbacks (assq-delete-all req-id naviel--ipc-callbacks)) (when (or (null err) (string= err "success")) (funcall cb data))))) (when event (naviel--ipc-handle-event event))) (error nil))) (defun naviel--ipc-handle-event (event) (cond ((string= event "end-file") (naviel--next-in-queue)) ((string= event "pause") (setq naviel--paused t) (naviel--refresh-footer)) ((string= event "unpause") (setq naviel--paused nil) (naviel--refresh-footer)))) (defun naviel--ipc-send (command &optional callback) (when (and naviel--ipc-process (process-live-p naviel--ipc-process)) (cl-incf naviel--ipc-request-id) (let* ((id naviel--ipc-request-id) (msg (append `(("command" . ,(vconcat command))) (when callback `(("request_id" . ,id))))) (json (concat (json-encode msg) "\n"))) (when callback (push (cons id callback) naviel--ipc-callbacks)) (condition-case err (process-send-string naviel--ipc-process json) (error (message "naviel IPC send: %s" (error-message-string err))))))) ;;; Playback position polling (defun naviel--start-position-timer () (naviel--stop-position-timer) (setq naviel--position-timer (run-with-timer naviel-position-poll-interval naviel-position-poll-interval #'naviel--poll-position))) (defun naviel--stop-position-timer () (when naviel--position-timer (cancel-timer naviel--position-timer) (setq naviel--position-timer nil))) (defun naviel--poll-position () (when (and naviel--ipc-process (process-live-p naviel--ipc-process)) (naviel--ipc-send '("get_property" "time-pos") (lambda (v) (setq naviel--elapsed (and (numberp v) v)) (naviel--refresh-footer))) (naviel--ipc-send '("get_property" "duration") (lambda (v) (setq naviel--duration (and (numberp v) v)))))) ;;; mpv process management (defun naviel--stop () (naviel--stop-position-timer) (when (and naviel--ipc-process (process-live-p naviel--ipc-process)) (delete-process naviel--ipc-process)) (setq naviel--ipc-process nil naviel--ipc-buffer "") (when (and naviel--process (process-live-p naviel--process)) (delete-process naviel--process)) (let ((sock (naviel--ipc-socket-path))) (when (file-exists-p sock) (ignore-errors (delete-file sock)))) (setq naviel--process nil naviel--paused nil naviel--current-song nil naviel--elapsed nil naviel--duration nil) (naviel--refresh-footer) (naviel--queue-render-if-live)) (defun naviel--play-url (url song-info) (naviel--stop) (setq naviel--current-song song-info naviel--paused nil naviel--elapsed nil naviel--duration nil) (let* ((sock (naviel--ipc-socket-path)) (proc (start-process "naviel-mpv" nil naviel-mpv-executable "--no-video" "--quiet" (format "--volume=%d" naviel--volume) (format "--input-ipc-server=%s" sock) url))) (setq naviel--process proc)) (run-with-timer 0.3 nil #'naviel--ipc-connect) (run-with-timer 0.6 nil #'naviel--start-position-timer) (naviel--refresh-footer) (message "Playing: %s - %s" (plist-get song-info :title) (plist-get song-info :artist))) (defun naviel--toggle-pause () (interactive) (if (and naviel--process (process-live-p naviel--process)) (naviel--ipc-send '("cycle" "pause")) (message "naviel: nothing is playing"))) (defun naviel--seek (seconds) (if (and naviel--process (process-live-p naviel--process)) (naviel--ipc-send `("seek" ,seconds "relative")) (message "naviel: nothing is playing"))) (defun naviel-seek-forward () (interactive) (naviel--seek naviel-seek-step)) (defun naviel-seek-backward () (interactive) (naviel--seek (- naviel-seek-step))) ;;; Queue management (defun naviel--play-queue-index (idx) (setq naviel--queue-index idx) (let* ((song (nth idx naviel--queue)) (url (naviel--stream-url (plist-get song :id)))) (naviel--play-url url song) (naviel--rerender-current-view) (naviel--queue-render-if-live))) (defun naviel--next-in-queue () (interactive) (pcase naviel-repeat-mode ('one (naviel--play-queue-index naviel--queue-index)) ('album (naviel--play-queue-index (mod (1+ naviel--queue-index) (max 1 (length naviel--queue))))) (_ (let ((next (1+ naviel--queue-index))) (if (< next (length naviel--queue)) (naviel--play-queue-index next) (naviel--stop) (message "naviel: end of queue")))))) (defun naviel--prev-in-queue () (interactive) (let ((prev (1- naviel--queue-index))) (if (>= prev 0) (naviel--play-queue-index prev) (message "naviel: at start of queue")))) (defun naviel--volume-up () (interactive) (setq naviel--volume (min 100 (+ naviel--volume naviel-volume-step))) (naviel--ipc-send `("set_property" "volume" ,naviel--volume)) (naviel--refresh-footer) (message "Volume: %d%%" naviel--volume)) (defun naviel--volume-down () (interactive) (setq naviel--volume (max 0 (- naviel--volume naviel-volume-step))) (naviel--ipc-send `("set_property" "volume" ,naviel--volume)) (naviel--refresh-footer) (message "Volume: %d%%" naviel--volume)) (defun naviel--append-songs-to-queue (songs label) (let ((was-empty (null naviel--queue))) (setq naviel--queue (append naviel--queue songs)) (when was-empty (setq naviel--queue-index 0)) (naviel--rerender-current-view) (naviel--queue-render-if-live) (message "naviel: appended %d track%s from \"%s\"" (length songs) (if (= 1 (length songs)) "" "s") label))) (defun naviel-queue-shuffle () (interactive) (when (null naviel--queue) (user-error "Queue is empty")) (let* ((vec (vconcat naviel--queue)) (len (length vec))) (dotimes (i (1- len)) (let* ((j (+ i (random (- len i)))) (tmp (aref vec i))) (aset vec i (aref vec j)) (aset vec j tmp))) (setq naviel--queue (append vec nil))) (when naviel--current-song (let* ((cur-id (plist-get naviel--current-song :id)) (pos (cl-position-if (lambda (s) (equal (plist-get s :id) cur-id)) naviel--queue))) (when pos (let ((cur (nth pos naviel--queue))) (setq naviel--queue (cons cur (append (seq-take naviel--queue pos) (seq-drop naviel--queue (1+ pos))))) (setq naviel--queue-index 0))))) (naviel--rerender-current-view) (naviel--queue-render-if-live) (message "naviel: queue shuffled (%d tracks)" (length naviel--queue))) ;;; API parse helpers (defun naviel--parse-artists (response) (let (result) (dolist (index (xml-get-children (naviel--get-child response 'artists) 'index) result) (dolist (a (xml-get-children index 'artist)) (push `(:id ,(naviel--attr a 'id) :name ,(naviel--attr a 'name) :albums ,(naviel--attr a 'albumCount)) result))) (nreverse result))) (defun naviel--parse-artist-albums (response) (let (result) (dolist (al (xml-get-children (naviel--get-child response 'artist) 'album) result) (push `(:id ,(naviel--attr al 'id) :name ,(naviel--attr al 'name) :year ,(naviel--attr al 'year)) result)) (nreverse result))) (defun naviel--parse-album-songs (response) (let (result) (dolist (s (xml-get-children (naviel--get-child response 'album) 'song) result) (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) :track ,(naviel--attr s 'track) :duration ,(naviel--attr s 'duration)) result)) (nreverse result))) (defun naviel--parse-search (response) (let* ((r3 (naviel--get-child response 'searchResult3)) artists albums songs) (dolist (a (xml-get-children r3 'artist)) (push `(:id ,(naviel--attr a 'id) :name ,(naviel--attr a 'name)) artists)) (dolist (al (xml-get-children r3 'album)) (push `(:id ,(naviel--attr al 'id) :name ,(naviel--attr al 'name) :artist ,(naviel--attr al 'artist)) albums)) (dolist (s (xml-get-children r3 'song)) (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) :duration ,(naviel--attr s 'duration)) songs)) `(:artists ,(nreverse artists) :albums ,(nreverse albums) :songs ,(nreverse songs)))) (defun naviel--parse-random-songs (response) (let (result) (dolist (s (xml-get-children (naviel--get-child response 'randomSongs) 'song) result) (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) :duration ,(naviel--attr s 'duration)) result)) (nreverse result))) (defun naviel--parse-playlists (response) (let (result) (dolist (pl (xml-get-children (naviel--get-child response 'playlists) 'playlist) result) (push `(:id ,(naviel--attr pl 'id) :name ,(naviel--attr pl 'name) :count ,(naviel--attr pl 'songCount) :duration ,(naviel--attr pl 'duration) :owner ,(naviel--attr pl 'owner)) result)) (nreverse result))) (defun naviel--parse-playlist-songs (response) (let (result) (dolist (s (xml-get-children (naviel--get-child response 'playlist) 'entry) result) (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) :track ,(naviel--attr s 'track) :duration ,(naviel--attr s 'duration)) result)) (nreverse result))) (defun naviel--parse-album-list2 (response) (let (result) (dolist (al (xml-get-children (naviel--get-child response 'albumList2) 'album) result) (push `(:id ,(naviel--attr al 'id) :name ,(naviel--attr al 'name) :artist ,(naviel--attr al 'artist) :year ,(naviel--attr al 'year)) result)) (nreverse result))) (defun naviel--parse-genres (response) (let (result) (dolist (g (xml-get-children (naviel--get-child response 'genres) 'genre) result) (let* ((children (xml-node-children g)) (text-node (seq-find #'stringp children)) (name (when text-node (string-trim text-node))) (songs (naviel--attr g 'songCount)) (albums (naviel--attr g 'albumCount))) (when (and name (not (string= name ""))) (push `(:name ,name :songs ,songs :albums ,albums) result)))) (nreverse result))) (defun naviel--parse-songs-by-genre (response) (let (result) (dolist (s (xml-get-children (naviel--get-child response 'songsByGenre) 'song) result) (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) :duration ,(naviel--attr s 'duration)) result)) (nreverse result))) (defun naviel--parse-lyrics (response) (let* ((lyrics-node (naviel--get-child response 'lyrics))) (if lyrics-node (let* ((children (xml-node-children lyrics-node)) (text-node (seq-find #'stringp children))) (if text-node (string-trim text-node) "No lyrics found.")) "No lyrics found."))) ;;; Async API entry points (defun naviel--get-artists (cb) (naviel--request-async "getArtists" '() #'naviel--parse-artists cb)) (defun naviel--get-artist (id cb) (naviel--request-async "getArtist" `(("id" . ,id)) #'naviel--parse-artist-albums cb)) (defun naviel--get-album (id cb) (naviel--request-async "getAlbum" `(("id" . ,id)) #'naviel--parse-album-songs cb)) (defun naviel--get-playlists (cb) (naviel--request-async "getPlaylists" '() #'naviel--parse-playlists cb)) (defun naviel--get-playlist (id cb) (naviel--request-async "getPlaylist" `(("id" . ,id)) #'naviel--parse-playlist-songs cb)) (defun naviel--get-album-list2 (type size cb) (naviel--request-async "getAlbumList2" `(("type" . ,type) ("size" . ,(number-to-string size))) #'naviel--parse-album-list2 cb)) (defun naviel--get-genres (cb) (naviel--request-async "getGenres" '() #'naviel--parse-genres cb)) (defun naviel--get-songs-by-genre (genre count offset cb) (naviel--request-async "getSongsByGenre" `(("genre" . ,genre) ("count" . ,(number-to-string count)) ("offset" . ,(number-to-string offset))) #'naviel--parse-songs-by-genre cb)) (defun naviel--search-async (q cb) (naviel--request-async "search3" `(("query" . ,q) ("artistCount" . "10") ("albumCount" . "10") ("songCount" . "20")) #'naviel--parse-search cb)) (defun naviel--get-random-songs (n cb) (naviel--request-async "getRandomSongs" `(("size" . ,(number-to-string n))) #'naviel--parse-random-songs cb)) (defun naviel--get-lyrics (artist title cb) (naviel--request-async "getLyrics" `(("artist" . ,artist) ("title" . ,title)) #'naviel--parse-lyrics cb)) ;;; Duration / string helpers (defun naviel--format-secs (secs) (if (and secs (numberp secs) (> secs 0)) (let* ((s (round secs)) (m (/ s 60)) (ss (% s 60))) (format "%d:%02d" m ss)) "--:--")) (defun naviel--trunc (s max) (if (and s (> (length s) max)) (concat (substring s 0 (- max 1)) "…") (or s ""))) ;;; Browser buffer (defun naviel--browser-buffer () (get-buffer-create "*Naviel*")) (defun naviel--make-breadcrumbs () (if (null naviel--browser-stack) "" (propertize (concat " " (mapconcat (lambda (v) (plist-get v :label)) (reverse naviel--browser-stack) " ❯ ") "\n") 'face 'naviel-breadcrumb-face))) (defun naviel--volume-bar (vol) (let* ((w 10) (f (round (* w (/ vol 100.0))))) (concat "[" (make-string f ?■) (make-string (- w f) ?-) (format "] %3d%%" vol)))) (defun naviel--now-playing-bar () (let* ((song naviel--current-song) (elap (naviel--format-secs naviel--elapsed)) (dur (naviel--format-secs (or naviel--duration (when song (let ((d (plist-get song :duration))) (when (and d (not (string= d ""))) (string-to-number d))))))) (qlen (length naviel--queue))) (concat "\n" (propertize (make-string (max 50 (- (window-width) 2)) ?─) 'face 'naviel-separator-face) "\n" (if song (format " %s %s\n %s\n %s\n" (if naviel--paused (propertize "⏸ " 'face 'naviel-paused-face) (propertize "▶ " 'face 'naviel-now-playing-face)) (propertize (naviel--trunc (plist-get song :title) 50) 'face 'naviel-now-playing-face) (propertize (concat (naviel--trunc (plist-get song :artist) 30) " • " (naviel--trunc (or (plist-get song :album) "") 30)) 'face 'naviel-song-meta-face) (propertize (format "%s / %s [Queue: %d/%d]" elap dur (1+ naviel--queue-index) qlen) 'face 'naviel-position-face)) (propertize " ■ Nothing playing\n" 'face 'shadow)) (format " %s repeat: %s\n" (propertize (format "vol %s" (naviel--volume-bar naviel--volume)) 'face 'naviel-song-meta-face) (propertize (naviel--repeat-label) 'face 'naviel-repeat-face)) (propertize " RET play a append SPC pause n/p skip [ ] seek +/- vol s search l lyrics ? help" 'face 'naviel-footer-face) "\n"))) (defconst naviel--footer-sep "\0naviel-footer\0") ;;; Core render (defun naviel--browser-render (heading items format-fn type &optional section-label) (setq naviel--current-view `(:type ,type :items ,items :heading ,heading :format-fn ,format-fn :section-label ,section-label)) (naviel--do-render heading items format-fn type section-label)) (defun naviel--do-render (heading items format-fn type section-label) (with-current-buffer (naviel--browser-buffer) (naviel-browser-mode) (let ((inhibit-read-only t) (saved-line (line-number-at-pos))) (erase-buffer) (insert (naviel--make-breadcrumbs)) (insert (propertize (format " %s\n" heading) 'face 'naviel-header-face)) (insert (propertize (make-string (min 80 (window-width)) ?─) 'face 'naviel-separator-face)) (insert "\n") (when section-label (insert (propertize (format "\n %s\n\n" section-label) 'face 'naviel-breadcrumb-face))) (if (null items) (insert (propertize " (no results)\n" 'face 'shadow)) (let ((idx 0)) (dolist (item items) (let* ((is-playing (and naviel--current-song (eq type 'song) (= idx naviel--queue-index) (equal (plist-get item :id) (plist-get naviel--current-song :id)))) (line (funcall format-fn item is-playing))) (insert (propertize line 'naviel-item item 'naviel-type type 'naviel-idx idx 'face (when is-playing 'naviel-now-playing-face))) (insert "\n")) (cl-incf idx)))) (insert (propertize naviel--footer-sep 'invisible t)) (insert (naviel--now-playing-bar)) (goto-char (point-min)) (forward-line (max 0 (- saved-line 1)))))) (defun naviel--rerender-current-view () (when (and naviel--current-view (get-buffer "*Naviel*")) (naviel--do-render (plist-get naviel--current-view :heading) (plist-get naviel--current-view :items) (plist-get naviel--current-view :format-fn) (plist-get naviel--current-view :type) (plist-get naviel--current-view :section-label)))) (defun naviel--refresh-footer () (naviel--rerender-current-view) (naviel--update-mode-line)) ;;; Format callbacks (defun naviel--format-artist (artist _p) (format " %-40s %s" (propertize (naviel--trunc (plist-get artist :name) 40) 'face 'naviel-artist-face) (let ((n (plist-get artist :albums))) (if (and n (not (string= n ""))) (propertize (format " (%s)" n) 'face 'naviel-count-face) "")))) (defun naviel--format-album (album _p) (format " %-40s %s" (propertize (naviel--trunc (plist-get album :name) 40) 'face 'naviel-album-face) (let ((y (plist-get album :year))) (if (and y (not (string= y ""))) (propertize y 'face 'naviel-year-face) "")))) (defun naviel--format-album-with-artist (album _p) (format " %-38s │ %-28s %s" (propertize (naviel--trunc (or (plist-get album :name) "?") 38) 'face 'naviel-album-face) (let ((a (plist-get album :artist))) (if (and a (not (string= a ""))) (propertize (naviel--trunc a 28) 'face 'naviel-artist-face) "")) (let ((y (plist-get album :year))) (if (and y (not (string= y ""))) (propertize (format " │ %s" y) 'face 'naviel-year-face) "")))) (defun naviel--format-song (song playing) (let* ((track (or (plist-get song :track) "")) (title (naviel--trunc (or (plist-get song :title) "?") 38)) (artist (naviel--trunc (or (plist-get song :artist) "") 26)) (dur (naviel--format-secs (let ((d (plist-get song :duration))) (when (and d (not (string= d ""))) (string-to-number d)))))) (format "%s %3s │ %-38s │ %-26s │ %s" (if playing (propertize "▶" 'face 'naviel-now-playing-face) " ") (propertize track 'face 'naviel-track-num-face) (propertize title 'face (if playing 'naviel-now-playing-face 'naviel-song-title-face)) (propertize artist 'face 'naviel-song-meta-face) (propertize dur 'face 'naviel-song-meta-face)))) (defun naviel--format-playlist (pl _p) (let* ((name (naviel--trunc (or (plist-get pl :name) "?") 40)) (count (plist-get pl :count)) (owner (plist-get pl :owner))) (format " %-40s %s %s" (propertize name 'face 'naviel-album-face) (if (and count (not (string= count ""))) (propertize (format " │ %s tracks" count) 'face 'naviel-count-face) "") (if (and owner (not (string= owner ""))) (propertize (format " │ %s" owner) 'face 'naviel-song-meta-face) "")))) (defun naviel--format-genre (genre _p) (let* ((name (naviel--trunc (or (plist-get genre :name) "?") 30)) (songs (plist-get genre :songs)) (albums (plist-get genre :albums))) (format " %-30s │ %s" (propertize name 'face 'naviel-artist-face) (if (and songs (not (string= songs ""))) (propertize (format "%s songs • %s albums" songs (or albums "?")) 'face 'naviel-count-face) "")))) ;;; Render entry points (defun naviel--render-artists (artists) (naviel--browser-render (format "Artists (%d)" (length artists)) artists #'naviel--format-artist 'artist)) (defun naviel--render-albums (albums name) (naviel--browser-render name albums #'naviel--format-album 'album (format "%d album%s" (length albums) (if (= 1 (length albums)) "" "s")))) (defun naviel--render-songs (songs context &optional section) (naviel--browser-render context songs #'naviel--format-song 'song (or section (format "%d track%s" (length songs) (if (= 1 (length songs)) "" "s"))))) ;;; Queue buffer (defvar naviel-queue-mode-map (let ((m (make-sparse-keymap))) (define-key m (kbd "RET") #'naviel-queue-jump) (define-key m (kbd "d") #'naviel-queue-remove) (define-key m (kbd "D") #'naviel-queue-clear) (define-key m (kbd "u") #'naviel-queue-move-up) (define-key m (kbd "n") #'naviel--next-in-queue) (define-key m (kbd "p") #'naviel--prev-in-queue) (define-key m (kbd "SPC") #'naviel--toggle-pause) (define-key m (kbd "s") #'naviel-queue-shuffle) (define-key m (kbd "l") #'naviel-lyrics-show) (define-key m (kbd "q") #'quit-window) m)) (define-derived-mode naviel-queue-mode special-mode "Naviel:Queue" "Major mode for the Naviel queue buffer." (setq buffer-read-only t) (hl-line-mode 1)) (defun naviel--queue-buffer () (get-buffer-create "*Naviel Queue*")) (defun naviel--queue-render-if-live () (when (get-buffer "*Naviel Queue*") (naviel--queue-render))) (defun naviel--queue-render () (with-current-buffer (naviel--queue-buffer) (naviel-queue-mode) (let ((inhibit-read-only t) (saved-line (line-number-at-pos))) (erase-buffer) (insert (propertize (format " Queue (%d track%s)\n" (length naviel--queue) (if (= 1 (length naviel--queue)) "" "s")) 'face 'naviel-header-face)) (insert (propertize (make-string (min 80 (window-width)) ?─) 'face 'naviel-separator-face)) (insert "\n\n") (if (null naviel--queue) (insert (propertize " (queue is empty)\n" 'face 'shadow)) (let ((idx 0)) (dolist (song naviel--queue) (let* ((playing (= idx naviel--queue-index)) (marker (if playing (propertize "▶ " 'face 'naviel-now-playing-face) " ")) (title (naviel--trunc (or (plist-get song :title) "?") 38)) (artist (naviel--trunc (or (plist-get song :artist) "") 24)) (dur (naviel--format-secs (let ((d (plist-get song :duration))) (when (and d (not (string= d ""))) (string-to-number d))))) (line (format "%s%3d. │ %-38s │ %-24s │ %s\n" marker (1+ idx) title artist dur))) (insert (propertize line 'face (when playing 'naviel-now-playing-face) 'naviel-queue-idx idx))) (cl-incf idx)))) (insert "\n") (insert (propertize " RET jump d remove D clear u move-up s shuffle l lyrics q quit" 'face 'naviel-footer-face)) (insert "\n") (goto-char (point-min)) (forward-line (max 0 (- saved-line 1)))))) (defun naviel-queue-show () (interactive) (naviel--queue-render) (pop-to-buffer (naviel--queue-buffer) '(display-buffer-reuse-window))) (defun naviel-queue-jump () (interactive) (let ((idx (get-text-property (point) 'naviel-queue-idx))) (if idx (naviel--play-queue-index idx) (user-error "No track at point")))) (defun naviel-queue-remove () (interactive) (let ((idx (get-text-property (point) 'naviel-queue-idx))) (unless idx (user-error "No track at point")) (setq naviel--queue (append (seq-take naviel--queue idx) (seq-drop naviel--queue (1+ idx)))) (when (and (> naviel--queue-index idx) (> naviel--queue-index 0)) (cl-decf naviel--queue-index)) (when (>= naviel--queue-index (length naviel--queue)) (setq naviel--queue-index (max 0 (1- (length naviel--queue))))) (naviel--queue-render) (naviel--rerender-current-view))) (defun naviel-queue-clear () (interactive) (when (yes-or-no-p "Clear the entire queue? ") (naviel--stop) (setq naviel--queue '() naviel--queue-index 0) (naviel--queue-render) (naviel--rerender-current-view))) (defun naviel-queue-move-up () (interactive) (let ((idx (get-text-property (point) 'naviel-queue-idx))) (unless idx (user-error "No track at point")) (when (= idx 0) (user-error "Already at the top")) (let ((prev (1- idx))) (cl-rotatef (nth idx naviel--queue) (nth prev naviel--queue)) (cond ((= naviel--queue-index idx) (setq naviel--queue-index prev)) ((= naviel--queue-index prev) (setq naviel--queue-index idx)))) (naviel--queue-render) (forward-line -1))) ;;; Browser mode (defvar naviel-browser-mode-map (let ((m (make-sparse-keymap))) (define-key m (kbd "RET") #'naviel--browser-enter) (define-key m (kbd "a") #'naviel--browser-append) (define-key m (kbd "SPC") #'naviel--toggle-pause) (define-key m (kbd "n") #'naviel--next-in-queue) (define-key m (kbd "p") #'naviel--prev-in-queue) (define-key m (kbd "+") #'naviel--volume-up) (define-key m (kbd "=") #'naviel--volume-up) (define-key m (kbd "-") #'naviel--volume-down) (define-key m (kbd "[") #'naviel-seek-backward) (define-key m (kbd "]") #'naviel-seek-forward) (define-key m (kbd "s") #'naviel-search) (define-key m (kbd "r") #'naviel-play-random) (define-key m (kbd "b") #'naviel--browser-back) (define-key m (kbd "g") #'naviel--browser-refresh) (define-key m (kbd "l") #'naviel-lyrics-show) (define-key m (kbd "q") #'naviel--browser-quit) (define-key m (kbd "?") #'naviel-help) (define-key m (kbd "R") #'naviel-toggle-repeat) (define-key m (kbd "Q") #'naviel-queue-show) (define-key m (kbd "S") #'naviel-queue-shuffle) (define-key m (kbd "P") #'naviel-playlists) (define-key m (kbd "N") #'naviel-recently-added) (define-key m (kbd "H") #'naviel-recently-played) (define-key m (kbd "G") #'naviel-genres) m)) (define-derived-mode naviel-browser-mode special-mode "Naviel" "Major mode for browsing and controlling Naviel playback." (setq buffer-read-only t) (setq-local revert-buffer-function #'naviel--browser-refresh) (hl-line-mode 1)) (defun naviel--item-at-point () (let ((type (get-text-property (point) 'naviel-type)) (item (get-text-property (point) 'naviel-item))) (when type (cons type item)))) (defun naviel--browser-enter () (interactive) (let ((entry (naviel--item-at-point))) (unless entry (user-error "No item at point")) (let ((type (car entry)) (item (cdr entry))) (pcase type ('artist (let ((id (plist-get item :id)) (name (plist-get item :name))) (push `(:type artist :data ,id :label ,name) naviel--browser-stack) (message "naviel: loading albums...") (naviel--get-artist id (lambda (albums) (if albums (naviel--render-albums albums name) (pop naviel--browser-stack)))))) ('album (let ((id (plist-get item :id)) (name (plist-get item :name))) (push `(:type album :data ,id :label ,name) naviel--browser-stack) (message "naviel: loading tracks...") (naviel--get-album id (lambda (songs) (if songs (progn (setq naviel--queue songs naviel--queue-index 0) (naviel--render-songs songs name)) (pop naviel--browser-stack)))))) ('playlist (let ((id (plist-get item :id)) (name (plist-get item :name))) (push `(:type playlist :data ,id :label ,name) naviel--browser-stack) (message "naviel: loading playlist...") (naviel--get-playlist id (lambda (songs) (if songs (progn (setq naviel--queue songs naviel--queue-index 0) (naviel--render-songs songs name)) (pop naviel--browser-stack)))))) ('genre (let* ((name (plist-get item :name)) (count (min naviel-genre-song-limit (string-to-number (or (plist-get item :songs) "50"))))) (push `(:type genre :data ,name :label ,name) naviel--browser-stack) (message "naviel: loading songs for genre \"%s\"..." name) (naviel--get-songs-by-genre name count 0 (lambda (songs) (if songs (progn (setq naviel--queue songs naviel--queue-index 0) (naviel--render-songs songs (format "Genre: %s" name) (format "%d tracks" (length songs)))) (pop naviel--browser-stack) (message "naviel: no songs found for genre \"%s\"" name)))))) ('song (naviel--play-queue-index (get-text-property (point) 'naviel-idx))) (_ (message "naviel: unknown type: %s" type)))))) (defun naviel--browser-append () (interactive) (let ((entry (naviel--item-at-point))) (unless entry (user-error "No item at point")) (let ((type (car entry)) (item (cdr entry))) (pcase type ('album (naviel--get-album (plist-get item :id) (lambda (songs) (when songs (naviel--append-songs-to-queue songs (plist-get item :name)))))) ('playlist (naviel--get-playlist (plist-get item :id) (lambda (songs) (when songs (naviel--append-songs-to-queue songs (plist-get item :name)))))) ('song (naviel--append-songs-to-queue (list item) (or (plist-get item :title) "?"))) (_ (message "naviel: cannot append type: %s" type)))))) (defun naviel--browser-back () (interactive) (cond ((null naviel--browser-stack) (message "naviel: already at top level")) ((= 1 (length naviel--browser-stack)) (setq naviel--browser-stack '()) (naviel--show-artists)) (t (pop naviel--browser-stack) (let* ((frame (car naviel--browser-stack)) (type (plist-get frame :type)) (id (plist-get frame :data)) (name (plist-get frame :label))) (pcase type ('artist (naviel--get-artist id (lambda (a) (when a (naviel--render-albums a name))))) ('album (naviel--get-album id (lambda (s) (when s (naviel--render-songs s name))))) ('playlist (naviel--get-playlist id (lambda (s) (when s (naviel--render-songs s name))))) ('genre (naviel--get-songs-by-genre id naviel-genre-song-limit 0 (lambda (s) (when s (naviel--render-songs s (format "Genre: %s" id)))))) (_ (naviel--show-artists))))))) (defun naviel--browser-refresh (&rest _) (interactive) (if (null naviel--browser-stack) (naviel--show-artists) (let* ((frame (car naviel--browser-stack)) (type (plist-get frame :type)) (id (plist-get frame :data)) (name (plist-get frame :label))) (pcase type ('artist (naviel--get-artist id (lambda (a) (when a (naviel--render-albums a name))))) ('album (naviel--get-album id (lambda (s) (when s (naviel--render-songs s name))))) ('playlist-list (naviel-playlists)) ('playlist (naviel--get-playlist id (lambda (s) (when s (naviel--render-songs s name))))) ('genre-list (naviel-genres)) ('genre (naviel--get-songs-by-genre id naviel-genre-song-limit 0 (lambda (s) (when s (naviel--render-songs s (format "Genre: %s" id)))))) ('album-list2 (naviel--get-album-list2 id naviel-album-list-size (lambda (als) (when als (naviel--browser-render name als #'naviel--format-album-with-artist 'album))))) (_ (naviel--show-artists)))))) (defun naviel--browser-quit () (interactive) (quit-window)) (defun naviel--show-artists () (message "naviel: loading library...") (naviel--get-artists (lambda (artists) (when artists (setq naviel--browser-stack '()) (naviel--render-artists artists))))) ;;; Mode line (defun naviel--mode-line-string () (if naviel--current-song (format " [%s] %s%s" (if naviel--paused "paused" "playing") (naviel--trunc (plist-get naviel--current-song :title) 28) (if (and naviel--elapsed naviel--duration) (format " %s/%s" (naviel--format-secs naviel--elapsed) (naviel--format-secs naviel--duration)) "")) "")) (defun naviel--update-mode-line () (force-mode-line-update t)) ;;; Public commands ;;;###autoload (defun naviel () (interactive) (unless (executable-find naviel-mpv-executable) (error "naviel: mpv not found on PATH")) (pop-to-buffer (naviel--browser-buffer)) (naviel--show-artists)) ;;;###autoload (defun naviel-lyrics-show () "Fetch and display lyrics for the song at point, or the currently playing song." (interactive) (let* ((entry (if (eq major-mode 'naviel-queue-mode) (let ((idx (get-text-property (point) 'naviel-queue-idx))) (if idx (cons 'song (nth idx naviel--queue)) nil)) (naviel--item-at-point))) (type (and entry (car entry))) (item (and entry (cdr entry))) (song (if (eq type 'song) item naviel--current-song))) (if (not song) (user-error "naviel: No song playing or at point") (let ((artist (plist-get song :artist)) (title (plist-get song :title))) (message "naviel: fetching lyrics for \"%s\"..." title) (naviel--get-lyrics artist title (lambda (lyrics) (with-current-buffer (get-buffer-create "*Naviel Lyrics*") (let ((inhibit-read-only t)) (erase-buffer) (special-mode) (local-set-key (kbd "q") #'quit-window) (insert (propertize (format " %s • %s\n" title artist) 'face 'naviel-header-face)) (insert (propertize (make-string (min 80 (window-width)) ?─) 'face 'naviel-separator-face)) (insert "\n\n ") (insert (replace-regexp-in-string "\n" "\n " (or lyrics "No lyrics found for this track."))) (insert "\n\n") (insert (propertize (make-string (min 80 (window-width)) ?─) 'face 'naviel-separator-face)) (insert "\n") (insert (propertize " Press 'q' to close." 'face 'naviel-footer-face)) (goto-char (point-min)))) (pop-to-buffer "*Naviel Lyrics*"))))))) ;;;###autoload (defun naviel-search (query) (interactive "sSearch Naviel: ") (when (string= query "") (user-error "Search query cannot be empty")) (message "naviel: searching for \"%s\"..." query) (naviel--search-async query (lambda (results) (when results (let ((artists (plist-get results :artists)) (albums (plist-get results :albums)) (songs (plist-get results :songs))) (pop-to-buffer (naviel--browser-buffer)) (naviel-browser-mode) (let ((inhibit-read-only t)) (erase-buffer) (insert (propertize (format " Search: %s\n" query) 'face 'naviel-header-face)) (insert (propertize (make-string (min 80 (window-width)) ?─) 'face 'naviel-separator-face)) (insert "\n") (cl-flet ((section (label items type fmt-fn) (when items (insert (propertize (format "\n %s\n" label) 'face 'naviel-breadcrumb-face)) (let ((idx 0)) (dolist (item items) (insert (propertize (funcall fmt-fn item nil) 'naviel-item item 'naviel-type type 'naviel-idx idx)) (insert "\n") (cl-incf idx)))))) (section "Artists" artists 'artist #'naviel--format-artist) (section "Albums" albums 'album #'naviel--format-album) (when songs (insert (propertize "\n Songs\n" 'face 'naviel-breadcrumb-face)) (setq naviel--queue songs naviel--queue-index 0) (let ((idx 0)) (dolist (s songs) (insert (propertize (naviel--format-song s (and naviel--current-song (equal (plist-get s :id) (plist-get naviel--current-song :id)))) 'naviel-item s 'naviel-type 'song 'naviel-idx idx)) (insert "\n") (cl-incf idx))) (message "naviel: %d song%s in queue" (length songs) (if (= 1 (length songs)) "" "s"))) (when (and (null artists) (null albums) (null songs)) (insert (propertize " No results found.\n" 'face 'shadow)))) (insert (propertize naviel--footer-sep 'invisible t)) (insert (naviel--now-playing-bar)) (goto-char (point-min)) (forward-line 3))))))) ;;;###autoload (defun naviel-play-random (&optional count) (interactive "P") (let ((n (if count (prefix-numeric-value count) 20))) (message "naviel: fetching %d random tracks..." n) (naviel--get-random-songs n (lambda (songs) (when songs (setq naviel--queue songs naviel--queue-index 0) (pop-to-buffer (naviel--browser-buffer)) (naviel--render-songs songs (format "Random - %d tracks" n)) (naviel--play-queue-index 0)))))) ;;;###autoload (defun naviel-stop () (interactive) (naviel--stop) (message "naviel: stopped")) ;;;###autoload (defun naviel-playlists () (interactive) (message "naviel: loading playlists...") (naviel--get-playlists (lambda (pls) (if (null pls) (message "naviel: no playlists found") (push `(:type playlist-list :data nil :label "Playlists") naviel--browser-stack) (naviel--browser-render (format "Playlists (%d)" (length pls)) pls #'naviel--format-playlist 'playlist))))) ;;;###autoload (defun naviel-recently-added (&optional size) (interactive "P") (let ((n (if size (prefix-numeric-value size) naviel-album-list-size))) (message "naviel: fetching recently added albums...") (naviel--get-album-list2 "newest" n (lambda (albums) (if (null albums) (message "naviel: no results") (push `(:type album-list2 :data "newest" :label "Recently Added") naviel--browser-stack) (naviel--browser-render (format "Recently Added (%d)" (length albums)) albums #'naviel--format-album-with-artist 'album)))))) ;;;###autoload (defun naviel-recently-played (&optional size) (interactive "P") (let ((n (if size (prefix-numeric-value size) naviel-album-list-size))) (message "naviel: fetching recently played albums...") (naviel--get-album-list2 "recent" n (lambda (albums) (if (null albums) (message "naviel: no results") (push `(:type album-list2 :data "recent" :label "Recently Played") naviel--browser-stack) (naviel--browser-render (format "Recently Played (%d)" (length albums)) albums #'naviel--format-album-with-artist 'album)))))) ;;;###autoload (defun naviel-genres () (interactive) (message "naviel: loading genres...") (naviel--get-genres (lambda (genres) (if (null genres) (message "naviel: no genres found") (setq genres (sort genres (lambda (a b) (> (string-to-number (or (plist-get a :songs) "0")) (string-to-number (or (plist-get b :songs) "0")))))) (push `(:type genre-list :data nil :label "Genres") naviel--browser-stack) (naviel--browser-render (format "Genres (%d)" (length genres)) genres #'naviel--format-genre 'genre))))) ;;;###autoload (defun naviel-help () (interactive) (with-help-window "*Naviel Help*" (princ "Naviel - Navidrome client for Emacs\n") (princ (make-string 52 ?─)) (princ "\n\n") (princ "Navigation\n") (princ " RET Play item / expand artist, album, playlist, or genre\n") (princ " a Append item to queue without replacing it\n") (princ " l Fetch and show lyrics for song at point\n") (princ " b Go back one level\n") (princ " g Refresh current view from server\n") (princ " s Search library\n") (princ " r Play random (C-u r for custom count)\n") (princ " q Quit browser\n\n") (princ "Library views\n") (princ " P Browse playlists\n") (princ " N Recently added albums (C-u N for custom count)\n") (princ " H Recently played albums (C-u H for custom count)\n") (princ " G Browse genres\n\n") (princ "Playback\n") (princ " SPC Toggle pause / resume\n") (princ " n Next track\n") (princ " p Previous track\n") (princ " [ Seek backward (naviel-seek-step seconds)\n") (princ " ] Seek forward (naviel-seek-step seconds)\n") (princ " + = Volume up\n") (princ " - Volume down\n") (princ " R Cycle repeat: off -> one -> all -> off\n\n") (princ "Queue\n") (princ " Q Toggle queue window\n") (princ " S Shuffle queue in place\n\n") (princ "Queue window\n") (princ " RET Jump to track\n") (princ " d Remove track from queue\n") (princ " D Clear entire queue\n") (princ " u Move track up one position\n\n"))) ;;; Mode line integration (defvar naviel-mode-line-format '(:eval (naviel--mode-line-string))) ;;;###autoload (define-minor-mode naviel-mode-line-mode "Show Naviel playback status in the mode line." :global t :lighter nil (if naviel-mode-line-mode (progn (add-to-list 'global-mode-string naviel-mode-line-format) (setq naviel--mode-line-timer (run-with-timer 1 1 #'naviel--update-mode-line))) (setq global-mode-string (remove naviel-mode-line-format global-mode-string)) (when naviel--mode-line-timer (cancel-timer naviel--mode-line-timer) (setq naviel--mode-line-timer nil)))) (provide 'naviel) ;;; naviel.el ends here