1

In dired-mode, I use an external quicklook tool on OSX called qlmanage, which pops open a window displaying a human readable image of the file with mouse scrolling capabilities. If I hit the space bar, qlmanage either displays the file or the process is deleted -- e.g., essentially an on/off switch.

I am having trouble figuring out how to move up or down in a dired-mode buffer, such that qlmanage will automatically display an image of the current line -- if an image is already being displayed -- but only do so after the previous process has successfully been killed with delete-process. A successful kill of the qlmanage process will return a process-exit-status of 9 or the event string will equal killed\n. I believe it is necessary to spawn a new process and sentinel when (= 9 (process-exit-status p); however, I am still confused by the nesting despite having had a few cups of coffee. I would like to have one function dired-qlmanage that serves both purposes -- i.e., on/off with the space bar, or show next image if one is already displayed when moving up or down.

CAVEAT:  The goal is to avoid having more than one active qlmanage process at any given moment in time, which is why I am using a sentinel.

FYI:  "Deleting a running process sends a signal to terminate it (and its child processes, if any), and calls the process sentinel." [Emphasis added.] https://www.gnu.org/software/emacs/manual/html_node/elisp/Deleting-Processes.html

(require 'dired)
(require 'cl)

(defun dired-kill-last-process-named (name)
"Function initially written by @wvxvw, and revised by @lawlist."
  (let (p)
    (cl-loop with name-re = 
             (format "^%s\\(?:<\\([[:digit:]]+\\)>\\)?" (regexp-quote name))
     for process in (process-list)
     for pname = (process-name process)
     if (string-match name-re pname)
     collect (cons (string-to-number (or (match-string 1 pname) "0")) process)
     into processes
     finally
      (let ((process (cdar (cl-sort processes '> :key 'car))))
        (when (and process (get-process process))
          (delete-process process)
          (setq p process)))) p))

(defun dired-qlmanage ()
(interactive)
  (unless (dired-kill-last-process-named "qlmanage")
    (let ((current-node (dired-get-file-for-visit)))
        (set-process-sentinel
          (start-process "qlmanage" nil "/usr/bin/qlmanage" "-p" current-node)
          (lambda (p e)
            (setq e (replace-regexp-in-string "\n$" "" e))
            (cond
              ((= 9 (process-exit-status p))
                ;; Should we spawn a new process with sentinel here?
                (message "killed (9):  %s | %s | %s" p e (process-exit-status p)))
              ((= 0 (process-exit-status p))
                (message "finished (0):  %s | %s | %s" p e (process-exit-status p)))
              (t
                (message "unknown (?):  %s | %s | %s" p e (process-exit-status p)))))))))

(defun my-dired-previous-line (arg)
(interactive "^p")
  (dired-previous-line arg)
  (dired-qlmanage))

(defun my-dired-next-line (arg)
(interactive "^p")
  (dired-next-line arg)
  (dired-qlmanage))

(defun my-dired-quicklook ()
(interactive)
  (dired-qlmanage))

(eval-after-load "dired" '(progn
  (define-key dired-mode-map [down] 'my-dired-next-line)
  (define-key dired-mode-map [up] 'my-dired-previous-line)
  (define-key dired-mode-map (kbd "SPC") 'my-dired-quicklook)))

1 Answer 1

1

qlmanage is a command-line utility built-in to OSX, so this answer only relates to using dired-mode with Emacs on OSX. I have left in the messages to help me better see what is happening, but they are not needed for this answer to work properly.

(require 'dired)
(require 'cl)

(defvar my-dired-spawn nil)
(make-variable-buffer-local 'my-dired-spawn)

;; http://stackoverflow.com/a/20023781/2112489
(defun my-dired-kill-last-process-named (name)
"Function initially written by @wvxvw, and revised by @lawlist."
  (let (p)
    (cl-loop with name-re = 
             (format "^%s\\(?:<\\([[:digit:]]+\\)>\\)?" (regexp-quote name))
     for process in (process-list)
     for pname = (process-name process)
     if (string-match name-re pname)
     collect (cons (string-to-number (or (match-string 1 pname) "0")) process)
     into processes
     finally
      (let ((process (cdar (cl-sort processes '> :key 'car))))
        (when (and process (get-process process))
          (delete-process process)
          (setq p process)))) p))

(defun my-dired-qlmanage ()
(interactive)
  (unless (my-dired-kill-last-process-named "qlmanage")
    (let* ((current-node (dired-get-file-for-visit)))
      (set-process-sentinel
        (start-process "qlmanage" nil "/usr/bin/qlmanage" "-p" current-node)
        (lambda (p e)
          (setq e (replace-regexp-in-string "\n$" "" e))
          (cond
            ((and (null my-dired-spawn) (= 9 (process-exit-status p)))
              (message "OFF: my-dired-qlmanage (%s) | %s | %s"
              (process-exit-status p) p e))
            ((and my-dired-spawn (= 9 (process-exit-status p)))
              (message "OFF/ON: my-dired-qlmanage (%s) | %s | %s"
              (process-exit-status p) p e)
              (my-dired-kill-spawn))
            ((= 0 (process-exit-status p))
              (message "OFF (mouse clicked): my-dired-qlmanage (%s) | %s | %s"
              (process-exit-status p) p e))
            (t
              (message "ABNORMAL: my-dired-qlmanage (%s) | %s | %s"
              (process-exit-status p) p e))))))))

(defun my-dired-kill-spawn ()
"This is essentially a three level incursion, starting with `my-dired-qlmanage'
and then calling `my-dired-kill-spawn' twice."
(interactive)
  (let* ((current-node (dired-get-file-for-visit)))
    (set-process-sentinel
      (start-process "qlmanage" nil "/usr/bin/qlmanage" "-p" current-node)
      (lambda (p e)
        (setq e (replace-regexp-in-string "\n$" "" e))
        (cond
          ((and (null my-dired-spawn) (= 9 (process-exit-status p)))
            (message "OFF: my-dired-kill-spawn (%s) | %s | %s"
              (process-exit-status p) p e))
          ((and my-dired-spawn (= 9 (process-exit-status p)))
            (message "OFF/ON: my-dired-kill-spawn (%s) | %s | %s"
              (process-exit-status p) p e)
            (my-dired-kill-spawn))
          ((= 0 (process-exit-status p))
            (message "OFF (mouse clicked): my-dired-kill-spawn (%s) | %s | %s"
              (process-exit-status p) p e))
          (t
            (message "ABNORMAL: my-dired-kill-spawn (%s) | %s | %s"
              (process-exit-status p) p e)))))))

(defun my-dired-previous-line (arg)
(interactive "^p")
  (dired-previous-line arg)
  (let ((my-dired-spawn t))
    (my-dired-kill-last-process-named "qlmanage")))

(defun my-dired-next-line (arg)
(interactive "^p")
  (dired-next-line arg)
  (let ((my-dired-spawn t))
    (my-dired-kill-last-process-named "qlmanage")))

(defun my-dired-quicklook ()
(interactive)
  (my-dired-qlmanage))

(eval-after-load "dired" '(progn
  (define-key dired-mode-map [down] 'my-dired-next-line)
  (define-key dired-mode-map [up] 'my-dired-previous-line)
  (define-key dired-mode-map (kbd "SPC") 'my-dired-quicklook)))

screenshot

7
  • Hey @lawlist, this is truly awesome! Thanks for sharing! I was wondering whether you are still using this code, or if it's been changed in these last 3 years? It works fine at my end, but I'd like to have quicklook not steal focus from emacs and perhaps auto-update on my-dired-next-line and my-dired-previous-line. If that would work I'd try to also get it working for helm-follow-mode.
    – Daniel
    Jun 28, 2019 at 22:28
  • @dangom -- I created a Github repository at github.com/lawlist/dired-qlmanage for the code that I am presently using, which appears to be the same (or at least very similar) to the code in the above accepted answer. In my tests on OSX Snow Leopard and El Capitan, Emacs retained focus when activating qlmanage from the dired-mode buffer. The image automatically updates when I use the up/down arrow keys to navigate to a new file in dired-mode. I do not use helm and therefore have no suggestions regarding how one might go about incorporating qlmanage into helm-related functions.
    – lawlist
    Jun 29, 2019 at 18:38
  • Hey @lawlist, thank you for sharing your code. It's really useful, specially for previewing files for which Emacs provides poor support, such as medical images. I've only tested the code under OSX High Sierra. The functions dired-qlmanage, my-dired-next|previous-line work just fine, but qlmanage steals focus when called.
    – Daniel
    Jun 29, 2019 at 18:41
  • @dangom -- Perhaps qlmanage on High Sierra has additional command-line options for foreground/background focus ... In a terminal window, you could try typing qlmanage -h and see if it has any new options to control the foreground/background focus behavior. I don't see any such options on OSX Snow Leopard, which is the main computer I use at home and work. Alternatively, you can write an applescript to return focus to Emacs ....
    – lawlist
    Jun 29, 2019 at 18:46
  • Yes, I was investigating that right now. It doesn't seem like qlmanage has changed. I think the easiest though perhaps not cleanest solution is to have Hammerspoon trigger when qlmanage is launched, and return focus to emacs. I have to do the same when using the org-capture firefox extension. Possibly an issue with High Sierra & above. =/
    – Daniel
    Jun 29, 2019 at 18:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.