1

I have a image displayed as inline image in org-mode. I would expect after the image file modified by others, emacs detect such modify and refresh the buffer to show the updated image.

How can I make that happen?

9
  • You'll need help from the OS: somebody will have to notice that the file has changed. On Linux, inotify can be used for such things - there are similar mechanisms on other OSes. Then you need a subprocess in Emacs that will get the notification and refresh the buffer. Commented Nov 29, 2023 at 17:20
  • 2
    There exists file-notify-add-watch which you can pass a callback function. You can read more about it here. Commented Nov 29, 2023 at 18:27
  • file-notify-add-watch does indeed make it easy. @dalanicolai: please make your comment into an answer. Commented Nov 30, 2023 at 4:20
  • @NickD I would argue that the suggestion to use file-notify-add-watch would only make up a small part of a complete answer for how to achieve it. A full answer would also provide code for the callback function, and possibly also some code to make org-mode create/manage such watchers automatically. Would you agree? Commented Nov 30, 2023 at 12:37
  • Yes. I think that the packaging would consist of the callback function, plus a function to set the whole thing up (the latter could be eval-ed by hand or through file local variables). It would have to save the Org mode file's buffer and the file-notify descriptor (probably as buffer-locals), the first so that the callback can run org-redisplay-inline-images in the correct buffer and the latter so that the watch can be removed. For now, removing the watch manually might be enough, but it could probably be done automatically when the buffer is killed. Commented Nov 30, 2023 at 16:06

1 Answer 1

2

[Updated answer after @dalanicolai's comments - this implements the lexically scoped solution which is most suitable if separate buffers are using their own separate images directory. See @dalanicolai's comment for another idea that is probably more suitable if there is a single images directory that is shared by multiple buffers. I did not implement that idea because I believe that the "separate images directories" setup is (or should be) the more common case - but that's IMO and YMMV.]

With thanks to @dalanicolai for pointing out the file notifications capabilities of Emacs, here is a fairly simple implementation:

;;; -*- lexical-binding: t -*-

(defun my/org-redisplay-create-file-watcher (imagedir)
  (interactive "f")
  (let ((my/org-redisplay-inline-images-buffer (current-buffer)))
    (cl-flet ((my/org-redisplay-inline-images-cb (_event)
                (with-current-buffer my/org-redisplay-inline-images-buffer
                  (org-redisplay-inline-images))))
      (setq-local my/org-redisplay-inline-images-file-watch (file-notify-add-watch imagedir '(change) #'my/org-redisplay-inline-images-cb)))))

(defun my/org-redisplay-delete-file-watcher ()
  (interactive)
  (if (boundp 'my/org-redisplay-inline-images-file-watch)
      (setq-local my/org-redisplay-inline-images-file-watch (file-notify-rm-watch my/org-redisplay-inline-images-file-watch))))

(add-hook 'kill-buffer-hook #'my/org-redisplay-delete-file-watcher)


While in your Org mode buffer, you can call my/org-redisplay-create-file-watcher with the path of the directory where the images are stored. It will create a file watcher watching that directory and it will store the watcher ID in a buffer-local variable, so that the watcher can be cleaned up when the buffer is killed. The watcher uses a callback function which calls org-redisplay-inline-images: whenever the watcher detects any change in the specified directory, it will invoke the callback which will redisplay the inline images. Note that any change will trigger a call: the callback does not know what files you are using and does not check that a relevant file has changed; it will fire a redisplay whatever the change (e.g. if somebody creates an unrelated file in the directory), but it will certainly catch changes to the relevant files and trigger a redisplay. You can limit the possibility of unnecessary redisplays by storing only the image files in the specified directory.

Note also that the buffer that you call the -create-... function from (the Org mode buffer with the images) is captured in the callback closure (note the lexical-binding setting at the top of the file) so that the redisplay can be done in the correct buffer (I have no idea what buffer is the current buffer when the callback is called). And we also add the cleanup function to the kill-buffer hook so that the watcher can be removed when the buffer is killed.

I saved the above code in a file redisplay.el, byte-compiled it and I tested it by starting with

emacs -Q -l redisplay.elc -l ob-python test.org

where test.org is shown below. It contains a local variables section at the end which automatically calls my/org-redisplay-create-file-watcher with an argument of ./images, a subdirectory of the current directory where the images are stored. After you confirm that it's OK to evaluate the local variables section, the buffer is opened. It will try to inline the images but since your ./images directory is currently empty, it will not succeed. The test.org file provides three python code blocks: the first two produce different images but they store the image they produce in the same file ./images/foo.org; the third code block uses a different file ./images.bar.org. So you can run the third code block with C-c C-c and then run alternately the first and second code blocks to change the image in the foo.org file and see that it is automatically updated.

Here's the test.org file:

#+STARTUP: inlineimages

* Some images


[[file:images/foo.png]]

and

[[file:images/bar.png]]


* Code

Two code blocks, each producing a PNG image named "images/foo.png" - depending
on which code block we run, we can change the image.


#+begin_src python :results silent file link :var file="./images/foo.png"
    
  import matplotlib.pyplot as plt

  fig, ax = plt.subplots()

  fruits = ['apple', 'blueberry', 'cherry', 'orange']
  counts = [40, 100, 30, 55]
  bar_labels = ['red', 'blue', '_red', 'orange']
  bar_colors = ['tab:red', 'tab:blue', 'tab:red', 'tab:orange']

  ax.bar(fruits, counts, label=bar_labels, color=bar_colors)

  ax.set_ylabel('fruit supply')
  ax.set_title('Fruit supply by kind and color')
  ax.legend(title='Fruit color')

  plt.savefig(file)
  return file
#+end_src


#+begin_src python :results silent file link :var file="./images/foo.png"
  import matplotlib.pyplot as plt
  import numpy as np


  def koch_snowflake(order, scale=10):
      """
      Return two lists x, y of point coordinates of the Koch snowflake.

      Parameters
      ----------
      order : int
          The recursion depth.
      scale : float
          The extent of the snowflake (edge length of the base triangle).
      """
      def _koch_snowflake_complex(order):
          if order == 0:
              # initial triangle
              angles = np.array([0, 120, 240]) + 90
              return scale / np.sqrt(3) * np.exp(np.deg2rad(angles) * 1j)
          else:
              ZR = 0.5 - 0.5j * np.sqrt(3) / 3

              p1 = _koch_snowflake_complex(order - 1)  # start points
              p2 = np.roll(p1, shift=-1)  # end points
              dp = p2 - p1  # connection vectors

              new_points = np.empty(len(p1) * 4, dtype=np.complex128)
              new_points[::4] = p1
              new_points[1::4] = p1 + dp / 3
              new_points[2::4] = p1 + dp * ZR
              new_points[3::4] = p1 + dp / 3 * 2
              return new_points

      points = _koch_snowflake_complex(order)
      x, y = points.real, points.imag
      return x, y


  x, y = koch_snowflake(order=5)

  plt.figure(figsize=(8, 8))
  plt.axis('equal')
  plt.fill(x, y)

  plt.savefig(file)
  return file
#+end_src


Here's another image - we produce it once and leave it alone thereafter:


#+begin_src python :results silent file link :var file="./images/bar.png"
    
import matplotlib.pyplot as plt
import numpy as np

# Fixing random state for reproducibility
np.random.seed(19680801)

dt = 0.01
t = np.arange(0, 30, dt)
nse1 = np.random.randn(len(t))                 # white noise 1
nse2 = np.random.randn(len(t))                 # white noise 2

# Two signals with a coherent part at 10 Hz and a random part
s1 = np.sin(2 * np.pi * 10 * t) + nse1
s2 = np.sin(2 * np.pi * 10 * t) + nse2

fig, axs = plt.subplots(2, 1, layout='constrained')
axs[0].plot(t, s1, t, s2)
axs[0].set_xlim(0, 2)
axs[0].set_xlabel('Time (s)')
axs[0].set_ylabel('s1 and s2')
axs[0].grid(True)

cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt)
axs[1].set_ylabel('Coherence')

plt.savefig(file)
return file
#+end_src


* Local variables
# Local Variables:
# eval: (my/org-redisplay-create-file-watcher "./images")
# End:

Incidentally, I use -l ob-python in the command line above to enable the Org Babel Python implementation, since the three code blocks that produce the images are written in python. The code is adapted from some examples found on the Matplotlib Gallery page.

I've done some more testing with the current (lexically-scoped) implementation, using two buffer with separate image subdirectories and modifying each subdirectory externally. It seems to work fine, but if you find problems, please let me know.

6
  • 1
    Very nice answer. A small question: would you agree that the 'current buffer' should not be stored as a buffer local variable? That variable is 'in theory' only known to the org buffer, so that when an image gets updated while you are working in some other buffer, the variable would be unknown. I guess it would be better to create a global alist mapping directories to the (list of) buffer(s) to update. Also, although python is quite ubiquitous, to make the answer not depend on matplotlib, you could use e.g. svg.el to create the images... Commented Dec 2, 2023 at 15:26
  • Yes, you are right: it doesn't work as is. I was trying to avoid globals but bufffer-local is too limiting. Commented Dec 3, 2023 at 17:43
  • I believe it should be doable with lexical scoping and minimal rearrangement, but I'm going to test a bit more before putting my foot in my mouth again... Commented Dec 3, 2023 at 19:35
  • 1
    Indeed it should be possible using lexical scoping also, but you would have to create multiple watchers if you are watching the same directory for multiple buffers (you could be showing files from a single dir in multiple org buffers). I think there is nothing wrong with a single 'org-watchers' global variable, where a single dir could be associated with multiple buffers (it would be a little more general solution). Of course, if you are assuming per org file 'dedicated' image directories, then you could also use the lexical solution. Commented Dec 3, 2023 at 20:42
  • 1
    I prefer the "separate images subdir for separate buffers" method (and that's what I implemented above using lexical scoping) but that's indeed an opinion. I just hope that it's correct this time :-) Thanks for your comments and for keeping me honest. Commented Dec 4, 2023 at 20:36

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.