;;; external-open.el --- Open Files Using Default OS Handler -*- lexical-binding: t; coding: utf-8; -*- ;; author Craig Jennings ;; ;;; Commentary: ;; ;; Layer: 2 (Core UX). ;; Category: L/D. ;; Load shape: eager. ;; Eager reason: command library with no side effects; eager only by init order. ;; A deferral candidate (autoload commands) for Phase 4. ;; Top-level side effects: none. ;; Runtime requires: host-environment, system-lib, external-open-lib, cl-lib. ;; Direct test load: yes (pure command helpers). ;; ;; This library provides a simple mechanism for opening files with specific ;; extensions using your operating system’s default application rather than ;; visiting them in an Emacs buffer. It offers: ;; ;; • A simple method to run a command on the current buffer's file ;; "C-c x o" bound to cj/open-this-file-with ;; • A customizable list =default-open-extensions= of file‐type suffixes ;; (e.g. “pdf”, “docx”, “png”) that should be handled externally. ;; • A function =default-open-file= (and its helper commands) which will ;; launch the matching file in the OS’s default MIME handler. ;; • Integration with =find-file-hook= so that any file whose extension ;; appears in =default-open-extensions= is automatically opened externally ;; upon visit. ;; • Optional interactive commands for manually invoking an external open on ;; point or on a user-chosen file. ;; ;;; Code: (require 'host-environment) ;; environment information functions (require 'system-lib) ;; for cj/file-from-context (require 'external-open-lib) ;; pure dispatch helpers (require 'cl-lib) ;; Declare platform-specific functions (declare-function w32-shell-execute "w32fns.c" (operation document &optional parameters show-flag)) (defgroup external-open nil "Open certain files with the OS default handler." :group 'files) (defcustom default-open-extensions '( ;; Video "\\.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\\'" ;; Audio "\\.aac\\'" "\\.ac3\\'" "\\.aif\\'" "\\.aifc\\'" "\\.aiff\\'" "\\.alac\\'" "\\.amr\\'" "\\.ape\\'" "\\.caf\\'" "\\.dff\\'" "\\.dsf\\'" "\\.flac\\'" "\\.m4a\\'" "\\.mka\\'" "\\.mid\\'" "\\.midi\\'" "\\.mp2\\'" "\\.mp3\\'" "\\.oga\\'" "\\.ogg\\'" "\\.opus\\'" "\\.ra\\'" "\\.spx\\'" "\\.wav\\'" "\\.wave\\'" "\\.weba\\'" "\\.wma\\'" ;; Microsoft Word "\\.docx?\\'" "\\.docm\\'" "\\.dotx?\\'" "\\.dotm\\'" "\\.rtf\\'" ;; Microsoft Excel "\\.xlsx?\\'" "\\.xlsm\\'" "\\.xlsb\\'" "\\.xltx?\\'" "\\.xltm\\'" ;; Microsoft PowerPoint "\\.pptx?\\'" "\\.pptm\\'" "\\.ppsx?\\'" "\\.ppsm\\'" "\\.potx?\\'" "\\.potm\\'" ;; Microsoft OneNote / Visio / Project / Access / Publisher "\\.one\\'" "\\.onepkg\\'" "\\.onetoc2\\'" "\\.vsdx?\\'" "\\.vsdm\\'" "\\.vstx?\\'" "\\.vstm\\'" "\\.vssx?\\'" "\\.vssm\\'" "\\.mpp\\'" "\\.mpt\\'" "\\.mdb\\'" "\\.accdb\\'" "\\.accde\\'" "\\.accdr\\'" "\\.accdt\\'" "\\.pub\\'" ;; OpenDocument (LibreOffice/OpenOffice) "\\.odt\\'" "\\.ott\\'" "\\.ods\\'" "\\.ots\\'" "\\.odp\\'" "\\.otp\\'" "\\.odg\\'" "\\.otg\\'" "\\.odm\\'" "\\.odf\\'" ;; Flat OpenDocument variants "\\.fodt\\'" "\\.fods\\'" "\\.fodp\\'" ;; Apple iWork "\\.pages\\'" "\\.numbers\\'" "\\.key\\'" ;; Microsoft’s fixed-layout formats "\\.xps\\'" "\\.oxps\\'" ) "Regexps matching file extensions that should be opened externally." :type '(repeat (regexp :tag "File extension regexp")) :group 'external-open) (defun cj/xdg-open (&optional filename) "Open FILENAME (or the file at point) with the OS default handler. Logs output and exit code to buffer *external-open.log*." (interactive) (let* ((file (expand-file-name (or (cj/file-from-context filename) (user-error "No file associated with this buffer")))) (cmd (or (cj/external-open-command) (user-error "External-open: unsupported host environment"))) (logbuf (get-buffer-create "*external-open.log*"))) (with-current-buffer logbuf (goto-char (point-max)) (insert (format-time-string "[%Y-%m-%d %H:%M:%S] ")) (insert (format "Opening: %s\n" file))) (cond ((env-windows-p) (w32-shell-execute "open" file)) (t (call-process cmd nil 0 nil file) (with-current-buffer logbuf (insert " → Launched asynchronously\n")))) nil)) ;; ------------------------------- Open File With ------------------------------ (defun cj/open-this-file-with (command) "Open this buffer's file with COMMAND, detached from Emacs." (interactive "MOpen with program: ") (unless buffer-file-name (user-error "Current buffer is not visiting a file")) (let ((file (expand-file-name buffer-file-name))) (cond ;; Windows: launch via ShellExecute so the child isn't tied to Emacs. ((env-windows-p) (w32-shell-execute "open" command (format "\"%s\"" file))) ;; POSIX: disown with nohup + background. No child remains. (t (call-process-shell-command (format "nohup %s %s >/dev/null 2>&1 &" command (shell-quote-argument file)) nil 0))))) ;; -------------------- 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." (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)))) (defun cj/external-open-install-advice () "Install the `cj/find-file-auto' advice on `find-file'. Idempotent: re-running removes any prior copy of the advice before adding it back, so re-evaluating the module updates the advice rather than stacking it." (advice-remove 'find-file #'cj/find-file-auto) (advice-add 'find-file :around #'cj/find-file-auto)) (cj/external-open-install-advice) (provide 'external-open) ;;; external-open.el ends here.