Existe-t-il un moyen d'exécuter une fonction de raccordement une seule fois?

15

Le contexte

J'utilise le after-make-frame-functionshook pour charger correctement les thèmes dans une configuration client / serveur emacs . Plus précisément, voici l'extrait de code que j'utilise pour le faire (basé sur cette réponse SO ):

 (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)))

Le problème

Lorsqu'une nouvelle emacsclient -c/tsession est démarrée, le hook est exécuté non seulement dans le nouveau cadre, mais dans tous les cadres existants existants (autres sessions emacsclient ) et cela fait un effet visuel très ennuyeux (les thèmes sont chargés à nouveau dans tous ces cadres) . Pire encore, dans le terminal, les clients déjà ouverts, la couleur du thème est complètement gâchée. Évidemment, cela ne se produit que sur les clients emacs connectés au même serveur emacs. La raison de ce comportement est claire, le hook est exécuté sur le serveur et tous ses clients sont affectés.

La question

Existe-t-il un moyen d'exécuter cette fonction une seule fois ou d'obtenir le même résultat sans utiliser le hook?


Une solution partielle

J'ai maintenant ce code, grâce à la réponse de @ Drew. Mais il y a toujours un problème, une fois que vous démarrez une session client dans le terminal, l'interface graphique ne charge pas les thèmes correctement et vice-versa. Après beaucoup de tests, j'ai réalisé que le comportement dépend de quel emacsclient commence en premier, et après avoir jeté diverses choses, je pense que c'est peut-être lié à la palette de couleurs qui est chargée. Si vous rechargez manuellement le thème, tout fonctionne bien et c'est la raison pour laquelle ce comportement n'apparaît pas lorsque la fonction est appelée par le hook à chaque fois comme dans ma configuration initiale.

(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)))

La solution finale

Enfin, j'ai un code totalement fonctionnel qui résout le comportement vu dans la solution partielle, pour y parvenir, j'exécute la fonction une fois par mode (terminal ou gui) lorsque le client pertinent est démarré pour la première fois, puis supprimez la fonction du crochet car est plus besoin. Maintenant, je suis heureux! :) Merci encore @Drew!

Le 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)))
joe di castro
la source
1
J'ai édité le titre comme suggéré. N'hésitez pas à revenir en arrière si ce n'est pas ce que vous vouliez dire à l'origine.
Malabarba
C'est bien @Malabarba! Je suis d'accord avec @drew
joe di castro

Réponses:

11

Je suppose que vous ne cherchez pas vraiment à " exécuter le hook une seule fois ". Je suppose que vous cherchez un moyen d'exécuter cette fonction particulière une seule fois, chaque fois que le hook est exécuté.

La réponse conventionnelle et simple à cette question est que votre fonction se retire du crochet, après avoir effectué l'action unique que vous souhaitez. En d'autres termes, utilisez add-hookdans un contexte où vous savez que la fonction doit être exécutée lorsque le hook est exécuté et que la fonction elle-même se supprime du hook, une fois que la fonction a fait son travail.

Si je devine correctement ce que vous voulez vraiment, envisagez de modifier votre question en quelque chose comme " Existe-t-il un moyen d'exécuter une fonction de raccordement une seule fois?

Voici un exemple, de la bibliothèque standard 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))
A dessiné
la source
Oui, cela peut fonctionner de cette façon, et je suppose que ce serait une solution moins problématique et sujette aux erreurs. Merci.
joe di castro
3
+1. Cela ne ferait pas de mal d'avoir un exemple sur 3 lignes d'une fonction qui se retire du crochet.
Malabarba
Enfin, j'ai une solution de travail totale basée partiellement sur votre réponse (à la fin, j'ai réalisé que je pouvais la dot sans supprimer la fonction du crochet). Merci beaucoup!
joe di castro
3

Voici une macro que vous pouvez utiliser à la place de add-hook(non largement testée):

(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))))

Remarque: make-symbolcrée un symbole non interne avec le nom donné. J'ai inclus un #dans le nom pour signaler le symbole comme quelque peu inhabituel, au cas où vous le rencontreriez en regardant une variable de crochet.

Harald Hanche-Olsen
la source
Ça ne marche pas pour moi, ça jette cette erreur:(void-function gensym)
joe di castro
@joedicastro Ah, oui, c'est du clpaquet. Désolé - j'oublie que tout le monde ne l'utilise pas. Vous pouvez utiliser à la (make-symbol "#once")place. Je mettrai à jour la réponse.
Harald Hanche-Olsen
1
J'ai essayé à nouveau et je n'ai pas travaillé pour moi, et honnêtement, comme j'avais une solution de travail partielle de Drew, je cherche plutôt ce chemin plus prometteur. Merci quand même pour votre réponse!
joe di castro
@joedicastro: C'est votre décision, bien sûr, et la solution de Drew fonctionnera en effet. Et c'est la manière conventionnelle de le faire. L'inconvénient principal est la nécessité de coder en dur le nom du hook dans la fonction hook, ce qui rend difficile la réutilisation de la fonction dans plusieurs hooks, si cela est nécessaire. De plus, si vous vous retrouvez à copier la solution pour l'utiliser dans un contexte différent, vous devez vous rappeler de modifier également le nom du crochet. Ma solution brise la dépendance, vous permettant de réutiliser les pièces et de les déplacer à volonté. Je suis curieux de savoir pourquoi cela ne fonctionne pas pour vous, mais si…
Harald Hanche-Olsen
… Mais si vous préférez ne pas prendre le temps d'aller au fond des choses, je comprends parfaitement. La vie est courte - choisissez ce qui vous convient.
Harald Hanche-Olsen
0

vous pouvez créer une hyper fonction gen-oncepour transposer une fonction normale en une fonction qui ne peut s'exécuter qu'une seule fois:

(setq lexical-binding t)

(defun gen-once (fn)
  (let ((done nil))
    (lambda () (if (not done)
                   (progn (setq done t)
                          (funcall fn))))))
(setq test (gen-once (lambda () (message "runs"))))

;;;;---following is test----;;;;
((lambda () (funcall test))) ;; first time, message "runs"
((lambda () (funcall test))) ;; nil
((lambda () (funcall test))) ;; nil

puis, utilisez (add-hook 'xxx-mode-hook (gen-once your-function-need-to-run-once))

chendianbuji
la source