summaryrefslogtreecommitdiff
path: root/modules/file-config.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-08-14 19:24:49 -0500
committerCraig Jennings <c@cjennings.net>2025-08-14 19:24:49 -0500
commit9278ddd4ea1a8b1a4c1edaa8894516e3f48d245b (patch)
tree1105519cd55a4ebbb1e91609e6aae7cc3929ddaf /modules/file-config.el
parenta878e5ae99f750ecbbb723f98ef91d3404189a32 (diff)
downloaddotemacs-9278ddd4ea1a8b1a4c1edaa8894516e3f48d245b.tar.gz
dotemacs-9278ddd4ea1a8b1a4c1edaa8894516e3f48d245b.zip
refactor(system-utils): major refactoring / adding tests
Theme: Modularize system-utilities into separate modules. Clean up any typos, buts, and unused variables. Add some initial ERT tests for new modules created. Changes: - Extract file handling into its own module (file-config) - Extract keyboard macro management into its own module (keyboard-macros) - Extract buffer burying (instead of killing) into its own module (undead-buffers) - Extract all date/time config into its own module (chrono-tools) - Moved keybinding discovery functionality and help into keybindings module - Combine flyspell and abbrev (spell-check and autocorrect) to flyspell-and-abbrev.el - Rename epa-config.el to auth-config.el for auth-source and epa settings. - Refactor `cj/kill-other-window` for more accurate buffer handling. - Include "*ert*" in the default bury (don't kill) list as killing it kills test runs. - Bind C-c M-m to inhibit-mouse-mode - Remove the unused ledger-file variable in user-constants.el. - Removed obsolete C-x x m, C-x x r, and C-x x d key mappings. - C-; b r to call cj/rename-buffer-and-file instead of typo’d function - Other purely cosmetic comment changes to system-utils.el ERT tests: - Rename ERT test definitions to include module scopes (file-config, keyboard-macros) - Add an ERT test for the timer bell's existence. - Add ERT tests to cover `cj/kill-buffer-or-bury-alive`, prefix-arg behavior, window-killing commands, and bulk operations. - Add test `authinfo-file` exists Missing authinfo triggers a debug message - Add test that `gpg2` executable is on the user’s PATH - Remove outdated authinfo test. - Add “Run these tests” note where missing.
Diffstat (limited to 'modules/file-config.el')
-rw-r--r--modules/file-config.el161
1 files changed, 161 insertions, 0 deletions
diff --git a/modules/file-config.el b/modules/file-config.el
new file mode 100644
index 00000000..cf5ef715
--- /dev/null
+++ b/modules/file-config.el
@@ -0,0 +1,161 @@
+;;; file-config.el --- Open Files Using Default OS Handler -*- lexical-binding: t; -*-
+;; author Craig Jennings <c@cjennings.net>
+;;
+;;; Commentary:
+;;
+;; 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
+
+;; ------------------------------- Open File With ------------------------------
+
+(defun cj/open-this-file-with (command)
+ "Asynchronously run COMMAND on the current buffer's file."
+ (interactive "MOpen with program: ")
+ (let ((display-buffer-alist
+ '(("\\*Async Shell Command\\*" display-buffer-no-window))))
+ (async-shell-command (format "%s \"%s\"" command buffer-file-name))))
+(global-set-key (kbd "C-c x o") #'cj/open-this-file-with)
+
+;; ------------------------- Use Default File Handlers -------------------------
+
+(defun cj/xdg-open-command ()
+ "Return the OS-default \"open\" command for this host.
+Signals an error if the host is unsupported."
+ (cond
+ ((env-linux-p) "xdg-open")
+ ((env-macos-p) "open")
+ ((env-windows-p) "start")
+ (t (error "cj/xdg-open: unsupported host environment"))))
+
+(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 *cj-xdg-open.log*."
+ (interactive)
+ (let* ((file (expand-file-name
+ (or filename
+ (dired-file-name-at-point))))
+ (cmd (cj/xdg-open-command))
+ (logbuf (get-buffer-create "*cj-xdg-open.log*"))
+ exit-code)
+ (with-current-buffer logbuf
+ (goto-char (point-max))
+ (insert (format-time-string "[%Y-%m-%d %H:%M:%S] " (current-time)))
+ (insert (format "Running `%s %s`\n" cmd file))
+ (setq exit-code
+ (call-process cmd nil logbuf t file))
+ (insert (format "Exit code: %d\n\n" exit-code)))
+ exit-code))
+
+(defun cj/find-file-auto (orig-fun &rest args)
+ "If file is media or Office, open via xdg-open, else call ORIG-FUN with ARGS."
+ (let ((file (car args))
+ (exts '("\\.avi\\'"
+ "\\.mp4\\'"
+ "\\.mkv\\'"
+ "\\.mov\\'"
+ "\\.mp3\\'"
+ "\\.ogg\\'"
+ "\\.docx?\\'"
+ "\\.pptx?\\'"
+ "\\.xlsx?\\'")))
+ (if (cl-find-if (lambda (re) (string-match re file)) exts)
+ (cj/xdg-open file)
+ (apply orig-fun args))))
+(advice-add 'find-file :around #'cj/find-file-auto)
+
+(provide 'file-config)
+;;; file-config.el ends here.
+
+;; --------------------------------- ERT Tests ---------------------------------
+;; Run these tests with M-x ert RET t RET
+
+(require 'ert)
+(require 'cl-lib)
+
+
+(ert-deftest file-config/open-this-file-with-invokes-async-shell-command ()
+ "Ensure `cj/open-this-file-with` calls `async-shell-command` with the
+program name and the buffer's file name, properly quoted."
+ (let* ((test-file (expand-file-name "space in name.txt"
+ temporary-file-directory))
+ called-cmd)
+ ;; Create a temp buffer and pretend its file is TEST-FILE
+ (with-temp-buffer
+ (set (make-local-variable 'buffer-file-name) test-file)
+ ;; Override `async-shell-command` to capture its argument
+ (cl-letf (((symbol-function 'async-shell-command)
+ (lambda (cmd &rest _)
+ (setq called-cmd cmd))))
+ (cj/open-this-file-with "myprog")))
+ ;; Now assert it was called with: myprog "full/path/to/space in name.txt"
+ (should (string= called-cmd
+ (format "myprog \"%s\"" test-file)))))
+
+(ert-deftest file-config/find-file-auto-opens-matching-extension ()
+ "cj/find-file-auto should invoke `cj/xdg-open' when the filename has a media/Office extension."
+ (let* ((called-file nil)
+ (test-file "/tmp/video.mp4")
+ ;; stash the real function so we can restore it later
+ (orig-xdg-open (symbol-function 'cj/xdg-open)))
+ (cl-letf (((symbol-function 'cj/xdg-open)
+ (lambda (file)
+ (setq called-file file)
+ ;; pretend we returned an exit code
+ 0)))
+ (unwind-protect
+ (progn
+ ;; call the advice wrapper as if Emacs were doing (find-file test-file)
+ (cj/find-file-auto
+ (lambda (_file) (error "Should not call original find-file"))
+ test-file)
+ (should (equal called-file test-file)))
+ ;; restore
+ (fset 'cj/xdg-open orig-xdg-open)))))
+
+(ert-deftest file-config/find-file-auto-falls-back-for-nonmatching ()
+ "cj/find-file-auto should fall back to the original FIND-FILE when the extension does not match."
+ (let ((called-file nil)
+ (test-file "/tmp/document.txt"))
+ (cj/find-file-auto
+ (lambda (file)
+ (setq called-file file)
+ ;; pretend that visiting succeeded
+ t)
+ test-file)
+ (should (equal called-file test-file))))
+
+(ert-deftest file-config/xdg-open-logs-and-returns-exit-code ()
+ "cj/xdg-open should call the OS command, log its invocation+exit code, and return that code."
+ (let* ((dummy-file (make-temp-file "cj-test" nil ".foo"))
+ (logbuf (get-buffer-create "*cj-xdg-open.log*"))
+ ;; override the command to something harmless
+ (orig-cmd-fn (symbol-function 'cj/xdg-open-command)))
+ (cl-letf (((symbol-function 'cj/xdg-open-command)
+ (lambda () "true")))
+ (unwind-protect
+ (let ((code (cj/xdg-open dummy-file)))
+ (should (= code 0))
+ (with-current-buffer logbuf
+ (goto-char (point-min))
+ (should (search-forward (format "`true %s`" dummy-file) nil t))
+ (should (search-forward "Exit code: 0" nil t))))
+ ;; cleanup
+ (kill-buffer logbuf)
+ (fset 'cj/xdg-open-command orig-cmd-fn)))))