bmag / emacs-purpose

Manage Windows and Buffers According to Purposes
GNU General Public License v3.0
496 stars 23 forks source link

How can I use shackle's rules as fallback? #124

Closed Alexander-Miller closed 6 years ago

Alexander-Miller commented 6 years ago

I'm currently testing the waters of enhancing my current shackle-based setup with purpose and ideally I'd want the two to play together. Right now (with the default spacemacs setup) the fallback for displaying a buffer when purpose cannot find a fitting window seems to ignore display-buffer-alist.

The result is that calling something like flycheck-list-errors (general purpose) when only a single elisp window (edit purpose) is open results in the elisp window being split horizontally and the error list now occupying the right half of my screen, contrary to how I told shackle to display it.

Alternatively is purpose able to replace the detailed popup rules that shackles provides? In the case of flycheck's error list that means 1) popping up the window at the bottom of the screen 2) giving it a height of 25% of the screen and 3) not selecting it.

bmag commented 6 years ago

I believe the two can work together.

First, you want to use shackle for displaying buffers. Purpose doesn't respect display-buffer-alist, but it offers purpose-special-action-sequences instead. Try this:

(defun purpose-shackle-match (_purpose buffer-or-name _alist)
  (shackle-match buffer-or-name))

(add-to-list 'purpose-special-action-sequences
             (cons 'purpose-shackle-match 'shackle-display-buffer-action))

Second, you'll want purpose not to reuse shackle windows for other buffers, so you'd want to give them a distinct purpose. You might also want different purposes for different sides. There is purpose-add-user-purposes for that.

Please tell me if it works out for you.

Alexander-Miller commented 6 years ago

Please tell me if it works out for you.

Spawning windows produces an error after running the code. Stack trace is at the bottom.

Second, you'll want purpose not to reuse shackle windows for other buffers, so you'd want to give them a distinct purpose.

Yeah, that's part 2 of the plan. Purpose controls window re-use, shackle controls windows placement and I want to make use of both. You know how Eclise or VS Code or Geany have some tab bar at the bottom where you'll usually find console, error list or test result windows. That's the layout I'm going for (at first).

Purpose can of course correctly re-use the error list window to spawn the Messages buffer when both major-modes share a purpose, placing these "side-show" buffers outside the main edit window, but I need shackle, or something like shackle, to correctly place the error list at the bottom in the first place.

I know there is an option to save and load predefined layouts, but I am typically switching too often for that, ẃorking on treemacs, then my dotfiles, then my theme, then ledger files, so I'd like my window configs to be quick to assemble and transform. There's also the issue of various temporary windows like help, org-edit, man or compile buffers that, from what I've learned so far, are best managed by shackle.

Debugger entered--Lisp error: (wrong-type-argument sequencep shackle-display-buffer-action)
  append(nil shackle-display-buffer-action (purpose-display-reuse-window-buffer purpose-display-reuse-window-purpose purpose-display-maybe-other-window purpose-display-maybe-pop-up-window purpose-display-maybe-other-frame purpose-display-maybe-pop-up-frame purpose-display-maybe-same-window))
  purpose--action-function(#<buffer *Flycheck errors*> nil)
  #f(compiled-function (buffer-or-name &optional action frame) "Display BUFFER-OR-NAME in some window, without selecting it.\nBUFFER-OR-NAME must be a buffer or the name of an existing\nbuffer.  Return the window chosen for displaying BUFFER-OR-NAME,\nor nil if no such window is found.\n\nOptional argument ACTION, if non-nil, should specify a display\naction.  Its form is described below.\n\nOptional argument FRAME, if non-nil, acts like an additional\nALIST entry (reusable-frames . FRAME) to the action list of ACTION,\nspecifying the frame(s) to search for a window that is already\ndisplaying the buffer.  See `display-buffer-reuse-window'.\n\nIf ACTION is non-nil, it should have the form (FUNCTION . ALIST),\nwhere FUNCTION is either a function or a list of functions, and\nALIST is an arbitrary association list (alist).\n\nEach such FUNCTION should accept two arguments: the buffer to\ndisplay and an alist.  Based on those arguments, it should\ndisplay the buffer and return the window.  If the caller is\nprepared to handle the case of not displaying the buffer\nand returning nil from `display-buffer' it should pass\n(allow-no-window . t) as an element of the ALIST.\n\nThe `display-buffer' function builds a function list and an alist\nby combining the functions and alists specified in\n`display-buffer-overriding-action', `display-buffer-alist', the\nACTION argument, `display-buffer-base-action', and\n`display-buffer-fallback-action' (in order).  Then it calls each\nfunction in the combined function list in turn, passing the\nbuffer as the first argument and the combined alist as the second\nargument, until one of the functions returns non-nil.\n\nIf ACTION is nil, the function list and the alist are built using\nonly the other variables mentioned above.\n\nAvailable action functions include:\n `display-buffer-same-window'\n `display-buffer-reuse-window'\n `display-buffer-pop-up-frame'\n `display-buffer-in-child-frame'\n `display-buffer-pop-up-window'\n `display-buffer-in-previous-window'\n `display-buffer-use-some-window'\n `display-buffer-use-some-frame'\n\nRecognized alist entries include:\n\n `inhibit-same-window' -- A non-nil value prevents the same\n                          window from being used for display.\n\n `inhibit-switch-frame' -- A non-nil value prevents any other\n                           frame from being raised or selected,\n                           even if the window is displayed there.\n\n `reusable-frames' -- Value specifies frame(s) to search for a\n                      window that already displays the buffer.\n                      See `display-buffer-reuse-window'.\n\n `pop-up-frame-parameters' -- Value specifies an alist of frame\n                              parameters to give a new frame, if\n                              one is created.\n\n `window-height' -- Value specifies either an integer (the number\n    of lines of a new window), a floating point number (the\n    fraction of a new window with respect to the height of the\n    frame's root window) or a function to be called with one\n    argument - a new window.  The function is supposed to adjust\n    the height of the window; its return value is ignored.\n    Suitable functions are `shrink-window-if-larger-than-buffer'\n    and `fit-window-to-buffer'.\n\n `window-width' -- Value specifies either an integer (the number\n    of columns of a new window), a floating point number (the\n    fraction of a new window with respect to the width of the\n    frame's root window) or a function to be called with one\n    argument - a new window.  The function is supposed to adjust\n    the width of the window; its return value is ignored.\n\n `allow-no-window' -- A non-nil value indicates readiness for the case\n    of not displaying the buffer and FUNCTION can safely return\n    a non-window value to suppress displaying.\n\n `preserve-size' -- Value should be either (t . nil) to\n    preserve the width of the window, (nil . t) to preserve its\n    height or (t . t) to preserve both.\n\n `window-parameters' -- Value specifies an alist of window\n                        parameters to give the chosen window.\n\nThe ACTION argument to `display-buffer' can also have a non-nil\nand non-list value.  This means to display the buffer in a window\nother than the selected one, even if it is already displayed in\nthe selected window.  If called interactively with a prefix\nargument, ACTION is t." (interactive #f(compiled-function () #<bytecode 0x3764fe9>)) #<bytecode 0x22b213>)("*Flycheck errors*" nil nil)
  purpose-display-buffer-advice(#f(compiled-function (buffer-or-name &optional action frame) "Display BUFFER-OR-NAME in some window, without selecting it.\nBUFFER-OR-NAME must be a buffer or the name of an existing\nbuffer.  Return the window chosen for displaying BUFFER-OR-NAME,\nor nil if no such window is found.\n\nOptional argument ACTION, if non-nil, should specify a display\naction.  Its form is described below.\n\nOptional argument FRAME, if non-nil, acts like an additional\nALIST entry (reusable-frames . FRAME) to the action list of ACTION,\nspecifying the frame(s) to search for a window that is already\ndisplaying the buffer.  See `display-buffer-reuse-window'.\n\nIf ACTION is non-nil, it should have the form (FUNCTION . ALIST),\nwhere FUNCTION is either a function or a list of functions, and\nALIST is an arbitrary association list (alist).\n\nEach such FUNCTION should accept two arguments: the buffer to\ndisplay and an alist.  Based on those arguments, it should\ndisplay the buffer and return the window.  If the caller is\nprepared to handle the case of not displaying the buffer\nand returning nil from `display-buffer' it should pass\n(allow-no-window . t) as an element of the ALIST.\n\nThe `display-buffer' function builds a function list and an alist\nby combining the functions and alists specified in\n`display-buffer-overriding-action', `display-buffer-alist', the\nACTION argument, `display-buffer-base-action', and\n`display-buffer-fallback-action' (in order).  Then it calls each\nfunction in the combined function list in turn, passing the\nbuffer as the first argument and the combined alist as the second\nargument, until one of the functions returns non-nil.\n\nIf ACTION is nil, the function list and the alist are built using\nonly the other variables mentioned above.\n\nAvailable action functions include:\n `display-buffer-same-window'\n `display-buffer-reuse-window'\n `display-buffer-pop-up-frame'\n `display-buffer-in-child-frame'\n `display-buffer-pop-up-window'\n `display-buffer-in-previous-window'\n `display-buffer-use-some-window'\n `display-buffer-use-some-frame'\n\nRecognized alist entries include:\n\n `inhibit-same-window' -- A non-nil value prevents the same\n                          window from being used for display.\n\n `inhibit-switch-frame' -- A non-nil value prevents any other\n                           frame from being raised or selected,\n                           even if the window is displayed there.\n\n `reusable-frames' -- Value specifies frame(s) to search for a\n                      window that already displays the buffer.\n                      See `display-buffer-reuse-window'.\n\n `pop-up-frame-parameters' -- Value specifies an alist of frame\n                              parameters to give a new frame, if\n                              one is created.\n\n `window-height' -- Value specifies either an integer (the number\n    of lines of a new window), a floating point number (the\n    fraction of a new window with respect to the height of the\n    frame's root window) or a function to be called with one\n    argument - a new window.  The function is supposed to adjust\n    the height of the window; its return value is ignored.\n    Suitable functions are `shrink-window-if-larger-than-buffer'\n    and `fit-window-to-buffer'.\n\n `window-width' -- Value specifies either an integer (the number\n    of columns of a new window), a floating point number (the\n    fraction of a new window with respect to the width of the\n    frame's root window) or a function to be called with one\n    argument - a new window.  The function is supposed to adjust\n    the width of the window; its return value is ignored.\n\n `allow-no-window' -- A non-nil value indicates readiness for the case\n    of not displaying the buffer and FUNCTION can safely return\n    a non-window value to suppress displaying.\n\n `preserve-size' -- Value should be either (t . nil) to\n    preserve the width of the window, (nil . t) to preserve its\n    height or (t . t) to preserve both.\n\n `window-parameters' -- Value specifies an alist of window\n                        parameters to give the chosen window.\n\nThe ACTION argument to `display-buffer' can also have a non-nil\nand non-list value.  This means to display the buffer in a window\nother than the selected one, even if it is already displayed in\nthe selected window.  If called interactively with a prefix\nargument, ACTION is t." (interactive #f(compiled-function () #<bytecode 0x376512b>)) #<bytecode 0x22b213>) "*Flycheck errors*")
  apply(purpose-display-buffer-advice #f(compiled-function (buffer-or-name &optional action frame) "Display BUFFER-OR-NAME in some window, without selecting it.\nBUFFER-OR-NAME must be a buffer or the name of an existing\nbuffer.  Return the window chosen for displaying BUFFER-OR-NAME,\nor nil if no such window is found.\n\nOptional argument ACTION, if non-nil, should specify a display\naction.  Its form is described below.\n\nOptional argument FRAME, if non-nil, acts like an additional\nALIST entry (reusable-frames . FRAME) to the action list of ACTION,\nspecifying the frame(s) to search for a window that is already\ndisplaying the buffer.  See `display-buffer-reuse-window'.\n\nIf ACTION is non-nil, it should have the form (FUNCTION . ALIST),\nwhere FUNCTION is either a function or a list of functions, and\nALIST is an arbitrary association list (alist).\n\nEach such FUNCTION should accept two arguments: the buffer to\ndisplay and an alist.  Based on those arguments, it should\ndisplay the buffer and return the window.  If the caller is\nprepared to handle the case of not displaying the buffer\nand returning nil from `display-buffer' it should pass\n(allow-no-window . t) as an element of the ALIST.\n\nThe `display-buffer' function builds a function list and an alist\nby combining the functions and alists specified in\n`display-buffer-overriding-action', `display-buffer-alist', the\nACTION argument, `display-buffer-base-action', and\n`display-buffer-fallback-action' (in order).  Then it calls each\nfunction in the combined function list in turn, passing the\nbuffer as the first argument and the combined alist as the second\nargument, until one of the functions returns non-nil.\n\nIf ACTION is nil, the function list and the alist are built using\nonly the other variables mentioned above.\n\nAvailable action functions include:\n `display-buffer-same-window'\n `display-buffer-reuse-window'\n `display-buffer-pop-up-frame'\n `display-buffer-in-child-frame'\n `display-buffer-pop-up-window'\n `display-buffer-in-previous-window'\n `display-buffer-use-some-window'\n `display-buffer-use-some-frame'\n\nRecognized alist entries include:\n\n `inhibit-same-window' -- A non-nil value prevents the same\n                          window from being used for display.\n\n `inhibit-switch-frame' -- A non-nil value prevents any other\n                           frame from being raised or selected,\n                           even if the window is displayed there.\n\n `reusable-frames' -- Value specifies frame(s) to search for a\n                      window that already displays the buffer.\n                      See `display-buffer-reuse-window'.\n\n `pop-up-frame-parameters' -- Value specifies an alist of frame\n                              parameters to give a new frame, if\n                              one is created.\n\n `window-height' -- Value specifies either an integer (the number\n    of lines of a new window), a floating point number (the\n    fraction of a new window with respect to the height of the\n    frame's root window) or a function to be called with one\n    argument - a new window.  The function is supposed to adjust\n    the height of the window; its return value is ignored.\n    Suitable functions are `shrink-window-if-larger-than-buffer'\n    and `fit-window-to-buffer'.\n\n `window-width' -- Value specifies either an integer (the number\n    of columns of a new window), a floating point number (the\n    fraction of a new window with respect to the width of the\n    frame's root window) or a function to be called with one\n    argument - a new window.  The function is supposed to adjust\n    the width of the window; its return value is ignored.\n\n `allow-no-window' -- A non-nil value indicates readiness for the case\n    of not displaying the buffer and FUNCTION can safely return\n    a non-window value to suppress displaying.\n\n `preserve-size' -- Value should be either (t . nil) to\n    preserve the width of the window, (nil . t) to preserve its\n    height or (t . t) to preserve both.\n\n `window-parameters' -- Value specifies an alist of window\n                        parameters to give the chosen window.\n\nThe ACTION argument to `display-buffer' can also have a non-nil\nand non-list value.  This means to display the buffer in a window\nother than the selected one, even if it is already displayed in\nthe selected window.  If called interactively with a prefix\nargument, ACTION is t." (interactive #f(compiled-function () #<bytecode 0x3765239>)) #<bytecode 0x22b213>) "*Flycheck errors*")
  display-buffer("*Flycheck errors*")
  flycheck-list-errors()
  (if window (quit-window nil window) (flycheck-list-errors))
  (let ((window (flycheck-get-error-list-window))) (if window (quit-window nil window) (flycheck-list-errors)))
  (-if-let* ((window (flycheck-get-error-list-window))) (quit-window nil window) (flycheck-list-errors))
  (-if-let (window (flycheck-get-error-list-window)) (quit-window nil window) (flycheck-list-errors))
  spacemacs/toggle-flycheck-error-list()
  funcall-interactively(spacemacs/toggle-flycheck-error-list)
  call-interactively(spacemacs/toggle-flycheck-error-list nil nil)
  command-execute(spacemacs/toggle-flycheck-error-list)
Alexander-Miller commented 6 years ago

Just changed the code to (cons 'purpose-shackle-match '(shackle-display-buffer-action)) and it seems to go well.

I'll do a bit of experimenting and then report back.

Alexander-Miller commented 6 years ago

Looks live I've discovered the next hurdle. Right now shackle is not only used as fallback, but also as the primary display system for windows that fit its rules.

For example if I have a messages window visible and then call flycheck-error-list a new window is created at the bottom of the screen even though both messages and error list window have the same purpose. So if a buffer matches one of shackle's rules it will be displayed based on its rule. However what I'm trying to achieve is for shackle's rules only to apply when a window cannot displayed based on its purpose.

bmag commented 6 years ago

Spawning windows produces an error

Just changed the code to (cons 'purpose-shackle-match '(shackle-display-buffer-action)) and it seems to go well.

Right, my bad. That's what I should have suggested.

Looks live I've discovered the next hurdle. Right now shackle is not only used as fallback, but also as the primary display system for windows that fit its rules.

That's what I was aiming for, I thought shackle is able to reuse its windows. To use shackle only as a fallback, you can use purpose-display-fallback instead of purpose-special-action-sequences. I'm not sure this is early enough in the display process, but it's worth a shot:

(setq purpose-display-fallback
      (lambda (buffer alist)
        (or (and (shackle-display-buffer-condition buffer alist)
                 (shackle-display-buffer-action buffer alist))
            (purpose-display-pop-up-window))))

(purpose-display-pop-up-window) is there as fallback for non-shackle buffers. You can omit it, and then the fallback for non-shackle buffers will be according to display-buffer-base-action and display-buffer-fallback-action.

Alexander-Miller commented 6 years ago

Sadly that's not it, the display-fallback is never used. I put first a print statement, then an error into the lambda. In either case the vanilla display fallback - the error buffer taking up the right half of the screen - was used.

Just to be clear, this is my current test code:

(setq purpose-display-fallback
      (lambda (buffer alist)
        (error "X")
        (or (and (shackle-display-buffer-condition buffer alist)
                 (shackle-display-buffer-action buffer alist))
            (purpose-display-pop-up-window))))
(setq purpose-user-mode-purposes
      '((flycheck-error-list-mode . bottom)
        (messages-buffer-mode     . bottom)))
(purpose-compile-user-configuration)
bmag commented 6 years ago

Hmm, this is probably because purpose manages to display the buffer in some way before trying the fallback - and why I wasn't sure it would work. If you look at the value of purpose-action-sequences dictates what purpose tries before going for the callback:

((switch-to-buffer . (purpose-display-reuse-window-buffer
                      purpose-display-reuse-window-purpose
                      purpose-display-maybe-same-window
                      purpose-display-maybe-other-window
                      purpose-display-maybe-other-frame
                      purpose-display-maybe-pop-up-window
                      purpose-display-maybe-pop-up-frame))
 (prefer-same-window . (purpose-display-maybe-same-window
                        purpose-display-reuse-window-buffer
                        purpose-display-reuse-window-purpose
                        purpose-display-maybe-other-window
                        purpose-display-maybe-other-frame
                        purpose-display-maybe-pop-up-window
                        purpose-display-maybe-pop-up-frame))
 (force-same-window . (purpose-display-maybe-same-window))
 (prefer-other-window . (purpose-display-reuse-window-buffer
                         purpose-display-reuse-window-purpose
                         purpose-display-maybe-other-window
                         purpose-display-maybe-pop-up-window
                         purpose-display-maybe-other-frame
                         purpose-display-maybe-pop-up-frame
                         purpose-display-maybe-same-window))
 (prefer-other-frame . (purpose-display-reuse-window-buffer-other-frame
                        purpose-display-reuse-window-purpose-other-frame
                        purpose-display-maybe-other-frame
                        purpose-display-maybe-pop-up-frame
                        purpose-display-maybe-other-window
                        purpose-display-maybe-pop-up-window
                        purpose-display-reuse-window-buffer
                        purpose-display-reuse-window-purpose
                        purpose-display-maybe-same-window)))

Probably purpose-display-maybe-pop-up-window is what displays the buffers and prevents the fallback. You can verify which display function are used by setting purpose-message-on-p and look what messages are produced in the messages buffer when displaying a shackle buffer.

Now it starts to get complicated: my next suggestion is to modify purpose-action-sequences so maybe-display-shackle appears after the reuse function and before the maybe functions:

(defun maybe-display-shackle (buffer alist)
  (and (shackle-display-buffer-condition buffer alist)
       (shackle-display-buffer-action buffer alist)))
Alexander-Miller commented 6 years ago

Yes, that works! I'll play with my setup some more, but so far it's working so well and there's so much synergy I think it should be part of spacemacs.