diff options
| author | Craig Jennings <c@cjennings.net> | 2025-08-14 19:24:49 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-08-14 19:24:49 -0500 |
| commit | 9278ddd4ea1a8b1a4c1edaa8894516e3f48d245b (patch) | |
| tree | 1105519cd55a4ebbb1e91609e6aae7cc3929ddaf /modules/file-config.el | |
| parent | a878e5ae99f750ecbbb723f98ef91d3404189a32 (diff) | |
| download | dotemacs-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.el | 161 |
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))))) |
