16

The context

I'm using the after-make-frame-functions hook to load properly the themes in an emacs client/server configuration. Specifically this is the code snippet that I use to make that (based in this SO answer):

(if (daemonp)
    (add-hook 'after-make-frame-functions
              (lambda (frame)
                (select-frame frame)
                (load-theme 'monokai t)
                ;; setup the smart-mode-line and its theme
                (sml/setup))) 
  (progn (load-theme 'monokai t)
         (sml/setup)))

The problem

When a new emacsclient -c/t session is started the hook is executed not only in the new frame, but in all the previous existent frames (other emacsclient sessions) and it makes a very annoying visual effect (the themes are loaded again in all those frames). Even worse, in the terminal clients already opened the theme color get completely messed up. Obviously that happens only on the emacs clients connected to the same emacs server. The reason for this behavior is clear, the hook is run on the server and all of its clients are affected.

The question

Is there any way to execute this function only once or getting the same result without using the hook?


A partial solution

I have now this code, thanks to the @Drew's answer. But still has a problem, once you start a client session in the terminal, the GUI does not load the themes properly and vice-versa. After a lot of tests, I realized that the behavior depends on which emacsclient starts first, and after discard various things, I think that it is maybe related with the color palette that is loaded. If you reload manually the theme all works fine and that's the reason why this behavior does not appear when the function is called by the hook every time as in my initial configuration.

(defun emacsclient-setup-theme-function (frame)
  (progn
    (select-frame frame)
    (load-theme 'monokai t)
    ;; setup the smart-mode-line and its theme
    (sml/setup)
    (remove-hook 'after-make-frame-functions 'emacsclient-setup-theme-function)))

(if (daemonp)
    (add-hook 'after-make-frame-functions 'emacsclient-setup-theme-function)
  (progn (load-theme 'monokai t)
         (sml/setup)))

The final solution

Finally I have totally working code that solves the behavior seen in the partial solution, to achieve this I run the function once by mode (terminal or gui) when then pertinent emacsclient is started for first time, then remove the function from the hook because is no needed any more. Now, I am happy! :) Thanks again @Drew!

The code:

(setq myGraphicModeHash (make-hash-table :test 'equal :size 2))
(puthash "gui" t myGraphicModeHash)
(puthash "term" t myGraphicModeHash)

(defun emacsclient-setup-theme-function (frame)
  (let ((gui (gethash "gui" myGraphicModeHash))
        (ter (gethash "term" myGraphicModeHash)))
    (progn
      (select-frame frame)
      (when (or gui ter) 
        (progn
          (load-theme 'monokai t)
          ;; setup the smart-mode-line and its theme
          (sml/setup)
          (sml/apply-theme 'dark)
          (if (display-graphic-p)
              (puthash "gui" nil myGraphicModeHash)
            (puthash "term" nil myGraphicModeHash))))
      (when (not (and gui ter))
        (remove-hook 'after-make-frame-functions 'emacsclient-setup-theme-function)))))

(if (daemonp)
    (add-hook 'after-make-frame-functions 'emacsclient-setup-theme-function)
  (progn (load-theme 'monokai t)
         (sml/setup)))
2
  • 1
    I've edited the title as suggested. Please, feel free to rollback if it is not what you originally meant.
    – Malabarba
    Commented Nov 9, 2014 at 18:20
  • It's fine @Malabarba! I agree with @drew Commented Nov 9, 2014 at 18:26

4 Answers 4

11

I'm guessing that you are not really looking for a way to "execute the hook only once". I'm guessing that you are looking for a way to execute that particular function only once, whenever the hook is run.

The conventional, and simple, answer to that question is for your function to remove itself from the hook, after carrying out the one-time action that you want. In other words use add-hook in a context where you know that the function should be executed when the hook is run, and have the function itself remove itself from the hook, after the function does its thing.

If I am guessing correctly about what you really want, then please consider editing your question to something like "Is there any way to run a hook function only once?

Here is an example, from standard library facemenu.el:

(defun facemenu-set-self-insert-face (face)
  "Arrange for the next self-inserted char to have face `face'."
  (setq facemenu-self-insert-data (cons face this-command))
  (add-hook 'post-self-insert-hook 'facemenu-post-self-insert-function))

(defun facemenu-post-self-insert-function ()
  (when (and (car facemenu-self-insert-data)
             (eq last-command (cdr facemenu-self-insert-data)))
    (put-text-property (1- (point)) (point)
                       'face (car facemenu-self-insert-data))
    (setq facemenu-self-insert-data nil))
  (remove-hook 'post-self-insert-hook 'facemenu-post-self-insert-function))
3
  • Yes, it can work in that way, and I suppose that it would be a less problematic and error-prone solution. Thanks. Commented Nov 9, 2014 at 18:25
  • 3
    +1. It wouldn't hurt to have a 3-line example of a function which removes itself from the hook.
    – Malabarba
    Commented Nov 9, 2014 at 18:37
  • Finally I have a total working solution partially based in your answer (at the end I realized that I could dot it without remove the function from the hook). Thank you very much! Commented Nov 10, 2014 at 17:55
4

Here is a macro that you can use instead of add-hook (not extensively tested):

(defmacro add-hook-run-once (hook function &optional append local)
  "Like add-hook, but remove the hook after it is called"
  (let ((sym (make-symbol "#once")))
    `(progn
       (defun ,sym ()
         (remove-hook ,hook ',sym ,local)
         (funcall ,function))
       (add-hook ,hook ',sym ,append ,local))))

Note: make-symbol creates an uninterned symbol with the given name. I included a # in the name to flag the symbol as somewhat unusual, in case you come across it while looking at a hook variable.

5
  • It does not work for me, it throws this error: (void-function gensym) Commented Nov 9, 2014 at 21:08
  • @joedicastro Ah, yes, that is from the cl package. Sorry about that – I forget that not everybody uses it. You can use (make-symbol "#once") instead. I'll update the answer. Commented Nov 9, 2014 at 21:34
  • 1
    I tried again, and didn't work for me, and honestly as I had a partial working solution from Drew, I seek that more promising path instead. Thanks anyway for you answer! Commented Nov 10, 2014 at 17:58
  • @joedicastro: That's your decision, of course, and indeed Drew's solution will work. And it's the conventional way to do it. The main drawback is the need to hard code the name of the hook in the hook function, making it hard to reuse the function in more than one hook, if that should be needed. Plus, if you find yourself copying the solution for use in a different context, you have to remember to also edit the name of the hook. My solution breaks the dependency, letting you reuse the pieces and move them around at will. I am curious as to why it doesn't work for you, but if … Commented Nov 10, 2014 at 21:35
  • … but if you'd rather not take the time to get to the bottom of that, I understand completely. Life is short – go with what works for you. Commented Nov 10, 2014 at 21:36
0

To achieve run "only once", you could using require. It allow you assure the code is executed only when the first time require, when you "provide" it in a file and add the contains folder path to load-path. I 'm using that way to execute what I want when deal with hook or advice.

0

It should be quite easy if you use the special operator letrec.

For example,

(letrec ((i-run-once
          (lambda ()
            (message "Run Once")
            (remove-hook 'post-self-insert-hook i-run-once))))
  (add-hook 'post-self-insert-hook i-run-once))

Your Answer

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

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