8

I sometimes want to display information in Help buffer style, so I have been using code like this:

(with-help-window (help-buffer)
  (princ "Type q to exit this help buffer.\n\n")
  (princ result))

This works fine, but the help window only uses half of my frame. I normally split my frame horizontally, to give two tall windows. The displayed help buffer uses one of the two windows.

I would rather use the whole frame in some cases, to display more information and to reduce the number of times I need to page down through the displayed information. The problem to solve is how to temporarily use the whole frame for the with-help-window call, and to automatically restore the original buffers/window sizes when I type "q" in the help window.

How can I best achieve this goal? I think I'm looking for something like this:

(with-FULL-FRAME-help-window (help-buffer)
   ...)

I've looked at winner-mode, bookmarks, saving layouts to registers, the various (and powerful, but complex) (display-buffer ...) methods. Most of them seem slightly off-target to my desired intent because they tend to correct/restore a layout after a full frame display operation. And it seems to me that many of them require me to manually restore my window layout (which I would prefer not to do).

I'm wondering if anyone has heard of a way to solve this simply. I'm hoping for something simple like these possible approaches, where I can override something with a let frame...

(let ((help-window-width-display-option fullwidth))
  (with-help-window (help-buffer)
    ...))

Or this kind of approach, which I don't know how to do yet, and which looks somewhat difficult/tricky for my current skill level.

(let ((save original configuration somehow)
  (delete-other-windows)
  (with-help-window (help-buffer)
     ...)
  ;; somehow, when I type "q" in the help buffer
  ;; - catch that action in code after the buffer is killed
  ;; - and restore the original window configuration
  )

It seems to me the key problem for me to solve is how to automatically restore the original window configuration when I type "q" in the temporary help-mode buffer. Thanks

Kevin
  • 1,308
  • 8
  • 20
  • One idea would be to use `display-buffer-pop-up-frame`: https://www.gnu.org/software/emacs/manual/html_node/elisp/Display-Action-Functions.html Another idea idea would be to issue a `make-frame` *while* using `display-buffer` with a custom function to target that new frame. If you are interested in locating and targeting an existing frame, then take a look at this example: http://stackoverflow.com/questions/18346785/how-to-intercept-a-file-before-it-opens-and-decide-which-frame – lawlist Jun 18 '16 at 16:12
  • Here is an idea of how to save and restore your window configuration so that you can use the existing frame: http://emacs.stackexchange.com/a/2714/2287 If you find that like like certain window configurations, you may wish to consider setting something up that is more elaborate -- there are several libraries that deal with saving and switching between various window configurations. – lawlist Jun 18 '16 at 16:24
  • As usual lawlist, thank you for your help. I have already tried `display-buffer-pop-up-frame`, since it is quite close to what I seek. But... the frame pops up in another place (not my current frame), and I must dispatch it with cmd-w, not "q" in help-style. Saving / restoring window configs is not the underlying problem. Currently I'm leaning toward cloning and modifying the source of with-help-window to give it an option that I can let-bind, or wrap with with a defmacro or something. I smile at how picky we emacs people are at wanting _exactly_ what we want from Emacs. – Kevin Jun 18 '16 at 16:41
  • After reading more in help.el, the solution seems to be buried somewhere in `help-return-method`, `quit-window`, the `quit-restore` window parameter, and probably some custom code to set/use all those things to create the desired effect. – Kevin Jun 18 '16 at 17:26

2 Answers2

5

Example #1: The keyboard shortcut q in the help-mode buffer comes from the special-mode-map that is incorporated into the help-mode-map. The default value is quit-window, which offers only four (4) possible actions: "According to information stored in WINDOW’s quit-restore window parameter either (1) delete WINDOW and its frame, (2) delete WINDOW, (3) restore the buffer previously displayed in WINDOW, or (4) make WINDOW display some other buffer than the present one. If non-nil, reset quit-restore parameter to nil." [See doc-string: M-x describe-function RET quit-window RET]

Here is an outline of what this example does:

  • Let-bind the variable help-window-select to t so that the *Help* window is selected.

  • Let-bind the current window configuration to a temporary variable called config.

  • Generate the *Help* window.

  • Store the previous window configuration -- config -- in a local-variable called my-stored-win-config.

  • Create a local key assignment for the letter q, which is bound to my-restore-win-config. [This local assignment trumps/shadows the previous assignment of quit-window.]

  • Delete other windows.

  • Press the letter q to restore the prior window configuration, and kill the *Help* buffer.

(defvar my-stored-win-config nil)
(make-variable-buffer-local 'my-stored-win-config)

(defun my-restore-win-config ()
  (interactive)
  (when my-stored-win-config
    (set-window-configuration my-stored-win-config)
    (kill-buffer "*Help*")))

The following snippet is a sample usage, but is not a complete interactive function. It can be evaluated in the *scratch* buffer to see it in action.

(let ((help-window-select t)
      (config (current-window-configuration)))
  (with-help-window (help-buffer)
    (princ "Type q to kill this *Help* buffer and restore prior window configuration."))
  (with-current-buffer "*Help*"
    (setq my-stored-win-config config)
    (local-set-key "q" 'my-restore-win-config))
  (delete-other-windows))

Example #2:

Here is a self-contained macro that does everything as the above example, which deals with three possible situations relating to existing hook values -- e.g., nil, symbol, or, list of symbols.

(defmacro help-window-full-frame (buffer-name &rest body)
"Doc-string."
  (declare (indent 1) (debug t))
  `(progn
    (set-marker help-window-point-marker nil)
      (let* (
          (help-window-select t)
          (foo
            (lambda ()
              (set (make-local-variable 'window-configuration)
                (current-window-configuration))
              (local-set-key "q"
                (lambda ()
                  (interactive)
                  (when window-configuration
                    ;; Record the `current-buffer' before it gets buried.
                    (let ((cb (current-buffer)))
                      (set-window-configuration window-configuration)
                      (kill-buffer cb)))))))
          ;; Preserve the original hook values by let-binding them in advance.
          ;; Otherwise, `add-to-list' would alter the global value.
          (temp-buffer-window-setup-hook temp-buffer-window-setup-hook)
          (temp-buffer-window-show-hook temp-buffer-window-show-hook)
          (temp-buffer-window-setup-hook
            (cond
              ((null temp-buffer-window-setup-hook)
                (list 'help-mode-setup foo))
              ((and
                  (not (null temp-buffer-window-setup-hook))
                  (listp temp-buffer-window-setup-hook))
                (add-to-list 'temp-buffer-window-setup-hook foo)
                (add-to-list 'temp-buffer-window-setup-hook 'help-mode-setup))
              ((and
                  (not (null temp-buffer-window-setup-hook))
                  (symbolp temp-buffer-window-setup-hook))
                (list 'help-mode-setup foo temp-buffer-window-setup-hook))))
          (temp-buffer-window-show-hook
            (cond
              ((null temp-buffer-window-show-hook)
                (list 'help-mode-finish 'delete-other-windows))
              ((and
                  (not (null temp-buffer-window-show-hook))
                  (listp temp-buffer-window-show-hook))
                (add-to-list 'temp-buffer-window-show-hook 'delete-other-windows)
                (add-to-list 'temp-buffer-window-show-hook 'help-mode-finish))
              ((and
                  (not (null temp-buffer-window-show-hook))
                  (symbolp temp-buffer-window-show-hook))
                (list
                  'help-mode-finish
                  'delete-other-windows
                  temp-buffer-window-show-hook)))) )
        (with-temp-buffer-window ,buffer-name nil 'help-window-setup (progn ,@body)))))

And here is the sample snippet to evaluate in the *scratch* buffer.

(help-window-full-frame (help-buffer)
  (princ "Type q to kill this *Help* buffer and restore prior window configuration."))
lawlist
  • 18,826
  • 5
  • 37
  • 118
  • Wow, thank you for an excellent answer. I had progressed to save/restore the window config, and had created a `my-help-quit` function, while trying to rebind the help-map key inside of `with-help-window`. But it wasn't working. I now see you bind the key inside the *Help* _buffer_ (not the Help window like I was doing) after the buffer is set up. I guess my binding was clobbered by the buffer setup. A lesson learned. Everything is working now. Many thanks. – Kevin Jun 18 '16 at 23:14
  • There are two (2) opportunities to act directly upon the `*Help*` buffer before it finishes -- the `temp-buffer-window-setup-hook` which runs `help-mode-setup` and then anything else already/previously assigned to the hook; and, then the `temp-buffer-window-show-hook` which runs `help-mode-finish` and anything already/previously assigned to the hook. `help-mode-setup` should remain first in time, but you could add something behind it by binding either one of those aforementioned hooks with custom stuff. In that scenario, you would not need `with-current-buffer`. – lawlist Jun 18 '16 at 23:49
  • Agreed. I looked at both `help-mode-setup` and `help-mode-finish`, but they both ran before the buffer was displayed. The key problem was to redirect the "q" keybinding, and you showed me how to do that in the buffer (not the window, which I was trying to do). PS. I tried to write a solution as `(defmacro with-full-frame-help-window`, but the macro still requires a separate function to handle the "q" and window restoration action. I will post my completed functions below. – Kevin Jun 18 '16 at 23:58
  • I updated the answer with a second example that uses a self-contained macro that does everything that the first example does. – lawlist Jun 19 '16 at 00:58
  • Wow again! I recognize lots of that code from the Emacs source. But I never even _thought_ of embedding the restoration function as a lambda function bound to the "q" key in my defmacro, or of storing the window config in a buffer local variable. And I didn't know how to stuff my restoration function into the setup/show hook chain either. My defmacro still needs my external restore function and variable. Many thanks again. My defmacros will be more sophisticated now, thanks to your efforts. – Kevin Jun 19 '16 at 01:22
  • could you please explain the thinking behind your use of `(setq temp-buffer-window-setup-hook (delq nil (list 'help-mode-setup foo temp-buffer-window-setup-hook)))? I don't understand the point of using `delq` when it is followed by nil. Unless you expect a nil in the hook list, and want to delete the nil. Can there be a nil in a hook list? And when I do simple test lists with the same syntax, the contents of the trailing `temp-buffer-window-setup-hook` in the list are not merged into a flat hook list with the new elements at the front. – Kevin Jun 19 '16 at 02:05
  • The values of both hooks by default are `nil`, but a user may have something attached. The original code contains a `cons` cell, which is limited to only two entries -- e.g., `(cons 'help-mode-setup temp-buffer-window-setup-hook)` I wanted more entries, and to have more than two entries, I needed to create a list without a 2-entry limit. The `cons` function automatically deletes a `nil` entry -- e.g., `(cons 'foo nil)` evaluates to `'('foo)`. When using `list`, a `nil` entry is *not* automatically deleted and we don't want to run a hook that is a `nil`. – lawlist Jun 19 '16 at 02:18
  • Thanks, that explains it, almost. But I still don't understand why `(list 'help-mode-setup foo temp-buffer-setup-hook)` works if the hook already has a list of functions in it. Sorry for the unformatted code, but this is what I get: `(setq hook (list 'one 'two)) (one two) (setq hook (delq nil (list 'help-setup 'foo hook))) (help-setup foo (one two))`. The original hook elements are in a sublist, not merged into the new hook list. Am I missing something? – Kevin Jun 19 '16 at 02:29
  • Your logic sounds correct -- i.e., that it would break if the user has a list already attached to the hook before running the macro. I'm open to any suggestions you may have regarding how to handle this dilemma by merging everything into one nice and neat list. :) Maybe we should use `append` or `push` or `add-to-list`, keeping in mind that we need to take care of the order? But we also need to check to see if it is a list before doing that -- `listp` or `symbolp`. – lawlist Jun 19 '16 at 02:33
  • Good to have your blessing that I'm on the right track. Here's what I get with my little experiment above: `(defmacro mergeme (&rest body) \`(progn (setq hook (list 'one 'two)) (setq hook (delq nil (list 'help-setup 'foo ,@hook))) )) (macroexpand '(mergeme)) (progn (setq hook (list (quote one) (quote two))) (setq hook (delq nil (list ... ... one two))))` See how the original hook functions one and two are flat now. – Kevin Jun 19 '16 at 02:41
  • This works for me: `(push mysetup temp-buffer-window-setup-hook) (push 'help-mode-setup temp-buffer-window-setup-hook) (delete-dups temp-buffer-window-setup-hook)`. I tested these operations with experiments, and put them into my defmacro because they preserved the desired order when duplicates were there and deleted, or when the original hook was empty. – Kevin Jun 19 '16 at 03:28
  • 1
    This also works for me, to replace the hardcoded "*Help*" buffer reference to the current buffer, because the restoration lambda is a buffer-local function. `... (kill-buffer (current-buffer))))))`. The macro took a buffer-name as argument, and killed "*Help*", so there could be a problem if the caller used a buffer whose name was different. I modified my macro to remove the `buffer-name` parameter, and generated/killed the same buffer inside the defmacro. – Kevin Jun 19 '16 at 03:56
  • I moved `(kill-buffer (current-buffer))` prior to the restoration of the window configuration. I updated the answer with 3 tests -- the existing hook is either nil, a symbol, or a list. The hooks are let-bound so they should not continue to grow in size with subsequent usage of the macro. Please let me know if I'm on the right track. – lawlist Jun 19 '16 at 04:20
  • It is nice working with you, for sure. My macro uses `push`, and so doesn't use tests at all, and it works fine. I noticed that when I used a temp buffer internal to the macro, the q-key binding (containing my window restore lambda) was shared by all other *Help* buffers. They did not set up the window config variable, so my restore function caused an error. So I now protect the window restore with `(when (boundp my-stored-window-config)`, instead of just the usual `(if my-stored-window-config`. – Kevin Jun 19 '16 at 04:50
  • Is there a conventional SO protocol for me showing my macro in the thread too, instead of trying to fit stuff into comments? I'd far rather edit my code posting like you get to do with yours... thanks – Kevin Jun 19 '16 at 04:51
  • I'm not familiar with using a *temp buffer internal* in a macro, so I'd need some more details to better understand the issue. In general, the people responding to questions are real programmers instead of just hobbyists such as myself -- and, consequently, the comments are much less when dealing with a professional programmer because the answers are usually correct at the outset, or are corrected within just a few comments. I'm slow, but I can usually get it eventually if I keep at it. In general, alternative answers are posted by the OP or as edits underneath the original question. – lawlist Jun 19 '16 at 05:20
  • Goodness me, if you are a hobbyist, then I have a lot to learn. I've been programming for 45 years, on and off, but not much elisp for 30 years... I will post my macro below my code below. Thanks for giving me permission on this site. :-) – Kevin Jun 19 '16 at 05:31
3

Based on the excellent answer by @lawlist, here are my completed functions for the next guy...

;; a tmp buffer-local place that gets destroyed with the help buffer
(defvar kwj-v-window-config-saved nil)
(make-variable-buffer-local 'kwj-v-window-config-saved)

(defun kwj-help-window-full-frame (string)
  "Show STRING in a help buffer using the full current frame."
  (let (original-layout)
    ;; set this before Help changes the config
    (setq original-layout (current-window-configuration))
    (with-help-window (help-buffer)
      (princ "Type q to exit this help buffer.\n\n")
      (princ string))
    (with-current-buffer "*Help*"
      ;; save layout in buffer local var that gets deleted
      (setq kwj-v-window-config-saved original-layout)
      ;; bind key in BUFFER (not in help window above)
      ;; bind key *after* help buf is displayed
      (local-set-key "q" 'kwj-help-window-restore))
    (delete-other-windows)))

(defun kwj-help-window-restore ()
  "Restore original windows after a full frame help display."
  (interactive)
  (set-window-configuration kwj-v-window-config-saved)
  (kill-buffer "*Help*"))

The long chain of comments above, with continued help from @lawlist, resulted in this version of a macro that doesn't require a buffer name, properly treats original setup/show hook lists, and that doesn't cause problems with the "q" key in other Help mode buffers.

(defmacro with-help-window-full-frame (&rest body)
  "Display text in a full-frame help window.
Execute BODY forms to put output into the window, with standard
output directed to the buffer."
  ;;tell indenter about this macro name
  (declare (indent 1))
  ;; must use a buffer string name here, not the buffer itself
  `(let ((mybuf ,(buffer-name (get-buffer-create "Full Frame Help")))
         ;;`(let ((mybuf ,(help-buffer))
         mysetup tmpsetup tmpshow)
     ;; save a copy of original hooks
     (setq tmpsetup (copy-list temp-buffer-window-setup-hook))
     (setq tmpshow (copy-list temp-buffer-window-show-hook))

     ;; create window config store and restore functions
     ;; credit to @lawlist on stackoverflow for this embedded setup
     (setq mysetup
           (lambda ()
             ;; store original window configuration
             (set (make-local-variable 'orig-win-config)
                  (current-window-configuration))
             ;; bind q to the window restore function
             (local-set-key
              "q"
              (lambda ()
                (interactive)
                ;; q is shared by all Help-mode buffers
                ;; so guard those that did not set up orig-win-config
                (when (boundp 'orig-win-config)
                  (set-window-configuration orig-win-config))
                (kill-buffer (current-buffer))))))

     ;; Add to help setup hooks. Keep original hook functions if any
     ;; delete-dups destructively hacks our tmp copy, not original hooklists
     (push mysetup tmpsetup)          ;order critical here
     (push 'help-mode-setup tmpsetup) ;this must be first in hook
     (delete-dups tmpsetup)

     (push 'help-mode-finish tmpshow) ;order not important here
     (push 'delete-other-windows tmpshow)
     (delete-dups tmpshow)

     ;; shadow the original hooks with our copies for the display call
     (let ((temp-buffer-window-setup-hook tmpsetup)
           (temp-buffer-window-show-hook tmpshow))

       ;; show buf with locally embedded window restore function
       (with-temp-buffer-window mybuf nil
                                'help-window-setup
                                (progn ,@body)))))

Use the macro in this way:

(with-help-window-full-frame
    (princ "Type q to exit this buffer."))
Kevin
  • 1,308
  • 8
  • 20