I forgot that I asked this question before, got frustrated by the slow link again, started googling, and this was the first page that came up in search. Not a good sign... So I did it. Hope someone would use it one day.
This relies on string keys in plist be eq to strings in choices: this is how plist-get works; the strings must not be copied. Not that I can think of a scenario where that would make sense; a reminder just in case.
If someone would want to make that into a package, please do. This can be generalized by attaching a concrete function (like this one which parses the menu and returns something that flyspell-correct-word-before-point expects to get from its x-popup-menu call) as a property of a command symbol ('flyspell-correct-word-before-point in this case).
(require 'cl-lib) (require 'cl-seq) (require 'ido) ;; This can be generalized by attaching a property to command symbol. (defun x-popup-menu--advice-around (f &rest args) (if (or (not (eq 'flyspell-correct-word-before-point this-command)) (ignore-errors (mouse-event-p (car args)))) (apply f args) ; Called via a mouse event, run normally. ;; Called via keyboard, do ido-completing-read. (let* ((mdesc (cadadr args)) (prompt (concat (car mdesc) ": ")) (pairwise (cdr mdesc)) ; (("foo" "foo") ... "" ... ) - may contain non-lists. (plist (nreverse (cl-reduce (lambda (acc x) (if (nlistp x) acc (cl-list* (cdr x) (car x) acc))) pairwise :initial-value nil))) ; ("foo" ("foo") ... ) (choices (nreverse (cl-reduce (lambda (acc x) (if (nlistp x) acc (cons (car x) acc))) pairwise :initial-value nil)))) ;; N.B.: every value in the plist is a 1-element list. (plist-get plist (ido-completing-read prompt choices nil t))))) (add-hook 'flyspell-mode-hook (lambda () (if (not flyspell-mode) (advice-remove 'x-popup-menu #'x-popup-menu--advice-around) (advice-add 'x-popup-menu :around #'x-popup-menu--advice-around) (define-key flyspell-mode-map [(control ?\.)] 'flyspell-correct-word-before-point))))