aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/elfeed-config.el19
-rw-r--r--modules/external-open.el73
-rw-r--r--modules/media-utils.el6
-rw-r--r--tests/test-external-open-commands.el85
-rw-r--r--tests/test-init-module-headers.el1
-rw-r--r--todo.org4
6 files changed, 165 insertions, 23 deletions
diff --git a/modules/elfeed-config.el b/modules/elfeed-config.el
index 7b4d7d745..eb2659ab5 100644
--- a/modules/elfeed-config.el
+++ b/modules/elfeed-config.el
@@ -65,11 +65,26 @@
;; Pivot with Kara Swisher and Scott Galloway
("https://www.youtube.com/feeds/videos.xml?channel_id=UCBHGZpDF2fsqPIPi0pNyuTg" yt pivot)
+ ;; Platypus Economics with Justin Wolfers
+ ("https://www.youtube.com/feeds/videos.xml?channel_id=UCB5eaPWEwR6wR2MxRx64s0g" yt platypus)
+
+ ;; Conversations with Tyler (Tyler Cowen)
+ ("https://www.youtube.com/feeds/videos.xml?channel_id=UC_AnpBvnhXTcipgGEHLWoOg" yt cwt)
+
+ ;; Plain English with Derek Thompson
+ ("https://www.youtube.com/feeds/videos.xml?channel_id=UCoOUW7SiXzLbc_O3nSDOBYA" yt plain-english)
+
+ ;; Odd Lots (Bloomberg) -- Joe Weisenthal & Tracy Alloway
+ ("https://www.youtube.com/feeds/videos.xml?playlist_id=PLe4PRejZgr0MuA6M0zkZyy-99-qc87wKV" yt oddlots)
+
+ ;; All-In Podcast
+ ("https://www.youtube.com/feeds/videos.xml?channel_id=UCESLZhusAkFfsNsApnjF_Cg" yt allin)
+
;; The Prof G Pod
("https://www.youtube.com/feeds/videos.xml?playlist_id=PLtQ-jBytlXCasRuBG86m22rOQfrEPcctq" yt profg)
;; On with Kara Swisher
- ("https://www.youtube.com/feeds/videos.xml?playlist_id=PLKof9YSAshgxI6odrEJFKsJbxamwoQBju" yt)
+ ("https://www.youtube.com/feeds/videos.xml?playlist_id=PLKof9YSAshgxI6odrEJFKsJbxamwoQBju" yt on)
;; Raging Moderates
("https://www.youtube.com/feeds/videos.xml?channel_id=UCcvDWzvxz6Kn1iPQHMl2teA" yt raging-moderates)
@@ -81,7 +96,7 @@
("https://www.youtube.com/feeds/videos.xml?playlist_id=PL45Mc1cDgnsB-u1iLPBYNF1fk-y1cVzTJ" yt trae)
;; Tropical Tidbits
- ("https://www.youtube.com/feeds/videos.xml?channel_id=UCrFIk7g_riIm2G2Vi90pxDA" yt)
+ ("https://www.youtube.com/feeds/videos.xml?channel_id=UCrFIk7g_riIm2G2Vi90pxDA" yt tropical)
;; If You're Listening | ABC News In-depth
("https://www.youtube.com/feeds/videos.xml?playlist_id=PLDTPrMoGHssAfgMMS3L5LpLNFMNp1U_Nq" yt listening)
diff --git a/modules/external-open.el b/modules/external-open.el
index 22e56a290..811c32c28 100644
--- a/modules/external-open.el
+++ b/modules/external-open.el
@@ -42,15 +42,33 @@
"Open certain files with the OS default handler."
:group 'files)
-(defcustom default-open-extensions
- '(
- ;; Video
- "\\.3g2\\'" "\\.3gp\\'" "\\.asf\\'" "\\.avi\\'" "\\.divx\\'" "\\.dv\\'"
+(defcustom cj/video-extensions
+ '("\\.3g2\\'" "\\.3gp\\'" "\\.asf\\'" "\\.avi\\'" "\\.divx\\'" "\\.dv\\'"
"\\.f4v\\'" "\\.flv\\'" "\\.m1v\\'" "\\.m2ts\\'" "\\.m2v\\'" "\\.m4v\\'"
"\\.mkv\\'" "\\.mov\\'" "\\.mpe\\'" "\\.mpeg\\'" "\\.mpg\\'" "\\.mp4\\'"
"\\.mts\\'" "\\.ogv\\'" "\\.rm\\'" "\\.rmvb\\'" "\\.vob\\'"
- "\\.webm\\'" "\\.wmv\\'"
+ "\\.webm\\'" "\\.wmv\\'")
+ "Regexps matching video files opened in a looping player.
+These route through `cj/open-video-looping' (mpv --loop-file=inf by default)
+instead of the OS default handler, so a video opened from dirvish plays on
+repeat."
+ :type '(repeat (regexp :tag "Video extension regexp"))
+ :group 'external-open)
+
+(defcustom cj/video-open-command "mpv"
+ "Player command used to open local video files on repeat.
+Launched detached from Emacs with `cj/video-open-args' before the file name."
+ :type 'string
+ :group 'external-open)
+
+(defcustom cj/video-open-args '("--loop-file=inf")
+ "Arguments passed to `cj/video-open-command' before the file name.
+Defaults to mpv's infinite single-file loop so the video plays on repeat."
+ :type '(repeat string)
+ :group 'external-open)
+(defcustom default-open-extensions
+ '(
;; Audio
"\\.aac\\'" "\\.ac3\\'" "\\.aif\\'" "\\.aifc\\'" "\\.aiff\\'"
"\\.alac\\'" "\\.amr\\'" "\\.ape\\'" "\\.caf\\'"
@@ -142,18 +160,49 @@ Logs output and exit code to buffer *external-open.log*."
nil 0)))))
+;; -------------------------- Open Videos On Repeat ----------------------------
+
+(defun cj/--video-file-p (file)
+ "Return non-nil when FILE matches a regexp in `cj/video-extensions'."
+ (and (stringp file)
+ (let ((case-fold-search t))
+ (cl-some (lambda (re) (string-match-p re file)) cj/video-extensions))))
+
+(defun cj/--video-open-arglist (file)
+ "Return the argument list to play FILE on repeat: `cj/video-open-args' + FILE."
+ (append cj/video-open-args (list file)))
+
+(defun cj/open-video-looping (&optional filename)
+ "Open FILENAME (or the file at point) in a looping video player, detached.
+Uses `cj/video-open-command' and `cj/video-open-args' (mpv --loop-file=inf by
+default) so the video plays on repeat. Launched asynchronously so it never
+blocks Emacs."
+ (interactive)
+ (let* ((file (expand-file-name
+ (or (cj/file-from-context filename)
+ (user-error "No file associated with this buffer"))))
+ (args (cj/--video-open-arglist file)))
+ (if (env-windows-p)
+ (w32-shell-execute "open" cj/video-open-command
+ (mapconcat (lambda (a) (format "\"%s\"" a)) args " "))
+ (apply #'call-process cj/video-open-command nil 0 nil args))))
+
;; -------------------- Open Files With Default File Handler -------------------
(defun cj/find-file-auto (orig-fun &rest args)
- "If file has an extension in `default-open-extensions', open externally.
-Else call ORIG-FUN with ARGS."
+ "Open FILE externally based on its extension, else call ORIG-FUN with ARGS.
+A video (`cj/video-extensions') opens in a looping player; any other extension
+in `default-open-extensions' opens with the OS default handler."
(let* ((file (car args))
(case-fold-search t))
- (if (and (stringp file)
- (cl-some (lambda (re) (string-match-p re file))
- default-open-extensions))
- (cj/xdg-open file)
- (apply orig-fun args))))
+ (cond
+ ((cj/--video-file-p file)
+ (cj/open-video-looping file))
+ ((and (stringp file)
+ (cl-some (lambda (re) (string-match-p re file))
+ default-open-extensions))
+ (cj/xdg-open file))
+ (t (apply orig-fun args)))))
(defun cj/external-open-install-advice ()
"Install the `cj/find-file-auto' advice on `find-file'.
diff --git a/modules/media-utils.el b/modules/media-utils.el
index 685530d89..1abbc1b2b 100644
--- a/modules/media-utils.el
+++ b/modules/media-utils.el
@@ -86,9 +86,11 @@ strings."
:value-type sexp))
:group 'media)
-(defcustom cj/default-media-player 'vlc
+(defcustom cj/default-media-player 'mpv
"The default media player to use for videos.
-Should be a key from `cj/media-players'."
+Should be a key from `cj/media-players'. mpv is the default because it
+resolves streaming-site URLs itself via yt-dlp, so it needs no pre-extracted
+stream URL (see the :needs-stream-url flag in `cj/media-players')."
:type 'symbol
:group 'media)
diff --git a/tests/test-external-open-commands.el b/tests/test-external-open-commands.el
index c0c83a340..3d8adc15e 100644
--- a/tests/test-external-open-commands.el
+++ b/tests/test-external-open-commands.el
@@ -81,8 +81,9 @@
;;; cj/find-file-auto
(ert-deftest test-external-open-find-file-auto-routes-media-externally ()
- "Normal: a `.mp4' filename (in `default-open-extensions') triggers
-`cj/xdg-open' instead of the original `find-file'."
+ "Normal: a non-video external extension (`.docx', in
+`default-open-extensions') triggers `cj/xdg-open' instead of the original
+`find-file'."
(let ((opened nil)
(orig-called nil))
(cl-letf (((symbol-function 'cj/xdg-open)
@@ -90,8 +91,23 @@
;; orig-fun replacement -- shouldn't run for a routed extension.
((symbol-function 'cj/find-file-auto--orig-stub)
(lambda (&rest _) (setq orig-called t))))
- (cj/find-file-auto #'cj/find-file-auto--orig-stub "/tmp/video.mp4"))
- (should (equal opened "/tmp/video.mp4"))
+ (cj/find-file-auto #'cj/find-file-auto--orig-stub "/tmp/report.docx"))
+ (should (equal opened "/tmp/report.docx"))
+ (should-not orig-called)))
+
+(ert-deftest test-external-open-find-file-auto-routes-video-to-looping-player ()
+ "Normal: a video filename triggers `cj/open-video-looping', not `cj/xdg-open'
+or the original `find-file'."
+ (let ((looped nil) (xdg nil) (orig-called nil))
+ (cl-letf (((symbol-function 'cj/open-video-looping)
+ (lambda (file) (setq looped file)))
+ ((symbol-function 'cj/xdg-open)
+ (lambda (_) (setq xdg t)))
+ ((symbol-function 'cj/find-file-auto--orig-stub)
+ (lambda (&rest _) (setq orig-called t))))
+ (cj/find-file-auto #'cj/find-file-auto--orig-stub "/tmp/clip.mp4"))
+ (should (equal looped "/tmp/clip.mp4"))
+ (should-not xdg)
(should-not orig-called)))
(ert-deftest test-external-open-find-file-auto-passes-through-text-files ()
@@ -116,5 +132,66 @@
(cj/find-file-auto #'cj/find-file-auto--orig-stub nil))
(should orig-called)))
+;;; cj/--video-file-p
+
+(ert-deftest test-external-open-video-file-p-matches-video ()
+ "Normal: common video extensions match, case-insensitively."
+ (should (cj/--video-file-p "/tmp/a.mp4"))
+ (should (cj/--video-file-p "/tmp/a.mkv"))
+ (should (cj/--video-file-p "/tmp/a.webm"))
+ (should (cj/--video-file-p "/tmp/A.MP4")))
+
+(ert-deftest test-external-open-video-file-p-rejects-non-video ()
+ "Boundary: audio, docs, and nil do not match."
+ (should-not (cj/--video-file-p "/tmp/a.mp3"))
+ (should-not (cj/--video-file-p "/tmp/a.txt"))
+ (should-not (cj/--video-file-p "/tmp/a.docx"))
+ (should-not (cj/--video-file-p nil)))
+
+;;; cj/--video-open-arglist
+
+(ert-deftest test-external-open-video-arglist-appends-file-after-args ()
+ "Normal: the player args precede the file in the argument list."
+ (let ((cj/video-open-args '("--loop-file=inf")))
+ (should (equal (cj/--video-open-arglist "/tmp/a.mp4")
+ '("--loop-file=inf" "/tmp/a.mp4")))))
+
+(ert-deftest test-external-open-video-arglist-respects-custom-args ()
+ "Boundary: custom args are honored; empty args yields just the file."
+ (let ((cj/video-open-args '("--loop=inf" "--mute=yes")))
+ (should (equal (cj/--video-open-arglist "/tmp/a.mkv")
+ '("--loop=inf" "--mute=yes" "/tmp/a.mkv"))))
+ (let ((cj/video-open-args nil))
+ (should (equal (cj/--video-open-arglist "/tmp/a.mkv") '("/tmp/a.mkv")))))
+
+;;; cj/open-video-looping
+
+(ert-deftest test-external-open-video-looping-calls-player-with-loop-args ()
+ "Normal: posix path calls the player with loop args + file, async (no wait)."
+ (let ((tmp (make-temp-file "test-ext-video-" nil ".mp4"))
+ (call nil))
+ (unwind-protect
+ (cl-letf (((symbol-function 'env-windows-p) (lambda () nil))
+ ((symbol-function 'call-process)
+ (lambda (prog _infile dest _disp &rest args)
+ (setq call (list prog dest args))
+ 0)))
+ (let ((cj/video-open-command "mpv")
+ (cj/video-open-args '("--loop-file=inf")))
+ (cj/open-video-looping tmp)))
+ (delete-file tmp))
+ (should (equal (nth 0 call) "mpv"))
+ (should (equal (nth 1 call) 0)) ; async destination: don't wait
+ (should (member "--loop-file=inf" (nth 2 call)))
+ (should (cl-find-if (lambda (a) (and (stringp a)
+ (string-match-p "\\.mp4\\'" a)))
+ (nth 2 call)))))
+
+(ert-deftest test-external-open-video-looping-errors-when-no-file ()
+ "Error: a buffer with no associated file signals user-error."
+ (with-temp-buffer
+ (cl-letf (((symbol-function 'cj/file-from-context) (lambda (_) nil)))
+ (should-error (cj/open-video-looping) :type 'user-error))))
+
(provide 'test-external-open-commands)
;;; test-external-open-commands.el ends here
diff --git a/tests/test-init-module-headers.el b/tests/test-init-module-headers.el
index 478819b89..22dec1d5f 100644
--- a/tests/test-init-module-headers.el
+++ b/tests/test-init-module-headers.el
@@ -129,7 +129,6 @@
"tramp-config"
"transcription-config"
"video-audio-recording"
- "term-config"
"weather-config"
"wrap-up")
"Modules annotated with the load-graph header contract.
diff --git a/todo.org b/todo.org
index 863749485..c7960068a 100644
--- a/todo.org
+++ b/todo.org
@@ -636,8 +636,8 @@ Make EAT the only terminal and remove ghostel entirely (decision 2026-06-25). Ph
- Phase 2 DONE (commit 0290b015): EAT experience settings in eat-config.el -- yank-to-terminal on, directory-tracking / prompt-annotations / command-history / mouse / kill-from-terminal / alt-screen affirmed, 10MB scrollback, truecolor already on via the compiled =eat-truecolor= terminfo. zsh shell-integration source line added to =~/.dotfiles/common/.zshrc= (uncommitted -- needs a dotfiles commit + a pull on the other daily driver).
- F12 = eshell-through-EAT (2026-06-25, commits cbd38d88 + c99fad28): F12 now opens eshell run through EAT (eat-eshell-mode) instead of a standalone EAT zsh shell, so the primary terminal is eshell (elisp functions as commands, TRAMP transparency) with EAT rendering visual commands. Retired eshell-toggle + xterm-color; added a zsh-parity prompt (git branch + [N] exit status) and a zoxide =z= sharing the zsh database. eat-config + eshell-config kept separate.
- Phase 3 DONE (commit 6c8f2a9c): ported ai-term from ghostel to EAT. The spike confirmed EAT + tmux detach/reattach behaves exactly like ghostel + tmux (eat spawns, sends =tmux new-session -A -s aiv-<project>=; killing the buffer leaves the session alive; respawn reattaches). The coupling was far smaller than feared -- most of the ~30 refs were comments, and agent detection is name-based ("agent [...]"), so backend-agnostic. Swaps: =(ghostel)= -> =(eat)= with =eat-buffer-name=, =ghostel-send-string= -> a process-send-string helper, M-SPC bound directly in =eat-semi-char-mode-map= (no exception/rebuild dance). 157 ai-term tests green. Real-agent launch + detach/reattach is a VERIFY under Manual testing and validation.
-- Phase 4 (now nearly unblocked -- ghostel's only remaining user is the dashboard launcher): retire ghostel. dashboard "Launch Terminal" =(ghostel)= -> open the eshell/EAT terminal; drop ghostel refs in =face-diagnostic.el= + =auto-dim-config.el=; migrate the useful term-config bits (tmux-history capture, copy surfaces -- both tmux-level, work under EAT) into eat-config; delete =term-config.el= and its init.el require; remove the pinned ghostel install.
-- Phase 5: cleanup. Remove the theme-studio ghostel app (=GHOSTEL_FACES=) once those faces are dead (ansi-color stays -- EAT inherits it); sweep ghostel mentions in comments/docs.
+- Phase 4 DONE (commit 6a9ec62e): retired ghostel. Migrated the terminal-generic keepers into eat-config -- the tmux copy-mode (=C-<up>= enters it, same UX + keybinding; agents run EAT over tmux so it's still tmux's own copy-mode) and the tmux-history capture, swapping =ghostel-send-string= -> a pty write and the mode checks -> eat-mode. Repointed the dashboard "Launch Terminal" to =cj/term-toggle=, swapped the =face-diagnostic= terminal-mode check to eat-mode, refreshed the auto-dim comment. Deleted =term-config.el= + its init require. EAT's default =eat-semi-char-non-bound-keys= already lets windmove / buffer-move / Emacs keys reach the terminal, so no exception-list port was needed. Tests retargeted (tmux-history 15/15). The copy-mode + tmux-history live check is a VERIFY under Manual testing and validation.
+- Phase 5 (remaining cleanup): remove the theme-studio ghostel app (=GHOSTEL_FACES=) now those faces are dead (ansi-color stays -- EAT inherits it); =package-delete= the unused ghostel ELPA package; sweep the few remaining ghostel mentions in comments/docs. Optional: surface F8/F10 in =eat-semi-char-non-bound-keys= if Craig misses them in agent buffers (needs the rebuild check).
** TODO [#C] ai-term.el commentary names a stale F9 keybinding scheme :quick:
The header commentary (lines ~43-64) still documents an old =F9= / =C-F9= / =s-F9= / =M-F9= scheme for =cj/ai-term= and its family, but those bindings no longer exist — F9 is unbound in the daemon and the only live global binding is =M-SPC= -> =cj/ai-term-next= (=ai-term.el:1059=). The =M-<f9>= mention in the =cj/ai-term-shutdown= docstring (~996) is stale too. Rewrite the commentary and any stale docstrings to match the current keymap. Found 2026-06-25 while scoping the F12 -> EAT work.