aboutsummaryrefslogtreecommitdiff
path: root/modules/system-utils.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-04-23 01:47:52 -0500
committerCraig Jennings <c@cjennings.net>2026-04-23 01:47:52 -0500
commitfb2593ad55d0523dc211019f7ec856d5898d7c99 (patch)
tree94c599d23f9e9bb3bca9d3cf0dc3c0043bc5dd39 /modules/system-utils.el
parent73a95c7d1c5ba591a3444f92012be4a281e8d08b (diff)
downloaddotemacs-fb2593ad55d0523dc211019f7ec856d5898d7c99.tar.gz
dotemacs-fb2593ad55d0523dc211019f7ec856d5898d7c99.zip
refactor(system-utils): extract testable open-file helpers
Extracts two pure helpers from cj/open-file-with-command and cj/xdg-open so the file-resolution and launcher-detection logic becomes testable without mocking process launchers. New helpers: - cj/--file-from-context returns a file path from the current context, resolving in priority order (explicit arg, buffer-file-name, dired file at point). Returns nil when none apply. - cj/--open-with-is-launcher-p is a predicate for whether a command is a desktop launcher (xdg-open, open, start) that needs call-process detachment. Both commands now delegate. cj/open-file-with-command uses cj/--file-from-context with read-file-name as the final fallback, plus cj/--open-with-is-launcher-p for the launcher dispatch. cj/xdg-open uses cj/--file-from-context with user-error as the "no file" fallback. Behavior preserved. The existing system-utils test suites still pass, and the shape of each command's final effect is identical. New tests, 14 cases across two per-function files: - tests/test-system-utils--file-from-context.el covers: explicit wins over buffer-file, explicit wins over dired, buffer-file fallback, dired fallback, all-nil returns nil, explicit-nil uses chain, dired-mode-but-no-file-at-point. - tests/test-system-utils--open-with-is-launcher-p.el covers: each of the three launcher names returns t, non-launcher returns nil, empty string returns nil, case-sensitive check, nil input returns nil. Coverage: system-utils.el went from 10/52 (19.2%) to 15/52 (28.8%). The remaining uncovered lines are mostly in the process-launching paths of cj/open-file-with-command and cj/xdg-open. Those are testability-blocked. Mocking call-process, start-process-shell-command, and generate-new-buffer would give a lot of mock surface for low value. cj/server-shutdown is not meaningfully testable because it kills Emacs.
Diffstat (limited to 'modules/system-utils.el')
-rw-r--r--modules/system-utils.el111
1 files changed, 57 insertions, 54 deletions
diff --git a/modules/system-utils.el b/modules/system-utils.el
index 29076f6d..86c2ae16 100644
--- a/modules/system-utils.el
+++ b/modules/system-utils.el
@@ -55,43 +55,50 @@
;;; ------------------------------- Open File With ------------------------------
;; TASK: Favor this method over cj/open-this-file-with and add to custom buffer funcs
+(defun cj/--file-from-context (&optional explicit-filename)
+ "Return a file path from the current context, or nil.
+Resolves in priority order:
+ 1. EXPLICIT-FILENAME, if non-nil.
+ 2. `buffer-file-name' of the current buffer.
+ 3. The file at point if the current buffer is in dired-mode.
+Returns nil when none of these yield a file."
+ (or explicit-filename
+ buffer-file-name
+ (and (derived-mode-p 'dired-mode)
+ (dired-file-name-at-point))))
+
+(defun cj/--open-with-is-launcher-p (command)
+ "Return non-nil if COMMAND is a desktop launcher.
+Launchers (xdg-open, open, start) need to be called with `call-process'
+and a zero BUFFER argument so they fully detach from Emacs. Other
+commands get `start-process-shell-command' so their output is visible."
+ (and (member command '("xdg-open" "open" "start")) t))
+
(defun cj/open-file-with-command (command)
"Open the current file with COMMAND.
-Works in both Dired buffers and regular file buffers. The command runs
-fully detached from Emacs."
+Works in both Dired buffers and regular file buffers. Prompts for a
+file only when neither context yields one. The command runs fully
+detached from Emacs."
(interactive "MOpen with command: ")
- (let* ((file (cond
- ;; In dired/dirvish mode, get file at point
- ((derived-mode-p 'dired-mode)
- (dired-get-file-for-visit))
- ;; In a regular file buffer
- (buffer-file-name
- buffer-file-name)
- ;; Fallback - prompt for file
- (t
- (read-file-name "File to open: "))))
- ;; For xdg-open and similar launchers, we need special handling
- (is-launcher (member command '("xdg-open" "open" "start"))))
- ;; Validate file exists
- (unless (and file (file-exists-p file))
- (error "No valid file found or selected"))
- ;; Use different approaches for launchers vs regular commands
- (if is-launcher
- ;; For launchers, use call-process with 0 to fully detach
- (progn
- (call-process command nil 0 nil file)
- (message "Opening %s with %s..." (file-name-nondirectory file) command))
- ;; For other commands, use start-process-shell-command for potential output
- (let* ((output-buffer-name (format "*Open with %s: %s*"
- command
- (file-name-nondirectory file)))
- (output-buffer (generate-new-buffer output-buffer-name)))
- (start-process-shell-command
- command
- output-buffer
- (format "%s %s" command (shell-quote-argument file)))
- (message "Running %s on %s..." command (file-name-nondirectory file))))))
-
+ (let* ((file (or (cj/--file-from-context)
+ (read-file-name "File to open: "))))
+ (unless (and file (file-exists-p file))
+ (error "No valid file found or selected"))
+ (if (cj/--open-with-is-launcher-p command)
+ (progn
+ (call-process command nil 0 nil file)
+ (message "Opening %s with %s..."
+ (file-name-nondirectory file) command))
+ (let* ((output-buffer-name (format "*Open with %s: %s*"
+ command
+ (file-name-nondirectory file)))
+ (output-buffer (generate-new-buffer output-buffer-name)))
+ (start-process-shell-command
+ command
+ output-buffer
+ (format "%s %s" command (shell-quote-argument file)))
+ (message "Running %s on %s..."
+ (file-name-nondirectory file) command)))))
(defun cj/identify-external-open-command ()
"Return the OS-default \"open\" command for this host.
@@ -107,26 +114,22 @@ Signals an error if the host is unsupported."
Logs output and exit code to buffer *external-open.log*."
(interactive)
(let* ((file (expand-file-name
- (or filename
- buffer-file-name
- (and (derived-mode-p 'dired-mode) (dired-file-name-at-point))
- (user-error "No file associated with this buffer"))))
- (cmd (cj/identify-external-open-command))
- (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
- ;; Windows: let the shell handle association; fully detached.
- ((env-windows-p)
- (w32-shell-execute "open" file))
- ;; macOS/Linux: run the opener synchronously; it returns immediately.
- (t
- (call-process cmd nil 0 nil file)
- (with-current-buffer logbuf
- (insert " → Launched asynchronously\n"))))
- nil))
+ (or (cj/--file-from-context filename)
+ (user-error "No file associated with this buffer"))))
+ (cmd (cj/identify-external-open-command))
+ (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))
;;; ------------------------------ Server Shutdown ------------------------------