Merge pull request #2372 from mickeynp/feature/improve-window-buffer-handling

Multiple buffer support and standardised window and buffer handling
This commit is contained in:
Dirk-Jan C. Binnema 2022-12-10 19:46:40 +02:00 committed by GitHub
commit a39d61bb58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 704 additions and 422 deletions

View File

@ -52,7 +52,8 @@ mu4e_srcs=[
'mu4e-speedbar.el',
'mu4e-update.el',
'mu4e-vars.el',
'mu4e-view.el'
'mu4e-view.el',
'mu4e-window.el'
]
# note, we cannot compile mu4e-config.el without incurring

View File

@ -76,7 +76,7 @@
(require 'mu4e-message)
(require 'mu4e-draft)
(require 'mu4e-context)
(require 'mu4e-window)
;;; Configuration
;; see mu4e-drafts.el
@ -443,9 +443,9 @@ buffers; lets remap its faces so it uses the ones for mu4e."
(subj (unless (and subj (string-match "^[:blank:]*$" subj)) subj))
(str (or subj
(pcase compose-type
('reply "*reply*")
('forward "*forward*")
(_ "*draft*")))))
('reply "*mu4e-reply*")
('forward "*mu4e-forward*")
(_ "*mu4e-draft*")))))
(rename-buffer (generate-new-buffer-name
(truncate-string-to-width
str mu4e~compose-buffer-max-name-length)
@ -502,8 +502,7 @@ See `mu4e-compose-crypto-policy' for more details."
(sign (mml-secure-message-sign))
(encrypt (mml-secure-message-encrypt)))))
(cl-defun mu4e~compose-handler (compose-type &optional original-msg includes
switch-function)
(cl-defun mu4e~compose-handler (compose-type &optional original-msg includes)
"Create a new draft message, or open an existing one.
COMPOSE-TYPE determines the kind of message to compose and is a
@ -539,79 +538,81 @@ are optional."
mu4e-compose-context-policy)
(run-hooks 'mu4e-compose-pre-hook)
;; this opens (or re-opens) a message with all the basic headers set.
(let ((winconf (current-window-configuration)))
(condition-case nil
(mu4e-draft-open compose-type original-msg switch-function)
(quit (set-window-configuration winconf)
(mu4e-message "Operation aborted")
(cl-return-from mu4e~compose-handler))))
;; insert mail-header-separator, which is needed by message mode to separate
;; headers and body. will be removed before saving to disk
(mu4e~draft-insert-mail-header-separator)
(let ((draft-buffer))
(let ((winconf (current-window-configuration)))
(condition-case nil
(setq draft-buffer (mu4e-draft-open compose-type original-msg))
(quit (set-window-configuration winconf)
(mu4e-message "Operation aborted")
(cl-return-from mu4e~compose-handler))))
(set-buffer draft-buffer)
;; insert mail-header-separator, which is needed by message mode to separate
;; headers and body. will be removed before saving to disk
(mu4e~draft-insert-mail-header-separator)
;; maybe encrypt/sign replies
(mu4e-compose-crypto-message original-msg compose-type)
;; maybe encrypt/sign replies
(mu4e-compose-crypto-message original-msg compose-type)
;; include files -- e.g. when inline forwarding a message with
;; attachments, we take those from the original.
(save-excursion
(goto-char (point-max)) ;; put attachments at the end
;; include files -- e.g. when inline forwarding a message with
;; attachments, we take those from the original.
(save-excursion
(goto-char (point-max)) ;; put attachments at the end
(if (and (eq compose-type 'forward) mu4e-compose-forward-as-attachment)
(mu4e-compose-attach-message original-msg)
(dolist (att includes)
(let ((file-name (plist-get att :file-name))
(mime (plist-get att :mime-type))
(description (plist-get att :description))
(disposition (plist-get att :disposition)))
(if file-name
(mml-attach-file file-name mime description disposition)
(mml-attach-buffer (plist-get att :buffer-name)
mime description disposition))))))
(if (and (eq compose-type 'forward) mu4e-compose-forward-as-attachment)
(mu4e-compose-attach-message original-msg)
(dolist (att includes)
(let ((file-name (plist-get att :file-name))
(mime (plist-get att :mime-type))
(description (plist-get att :description))
(disposition (plist-get att :disposition)))
(if file-name
(mml-attach-file file-name mime description disposition)
(mml-attach-buffer (plist-get att :buffer-name)
mime description disposition))))))
(mu4e~compose-set-friendly-buffer-name compose-type)
(mu4e~compose-set-friendly-buffer-name compose-type)
;; bind to `mu4e-compose-parent-message' of compose buffer
(set (make-local-variable 'mu4e-compose-parent-message) original-msg)
(put 'mu4e-compose-parent-message 'permanent-local t)
;; set mu4e-compose-type once more for this buffer,
(set (make-local-variable 'mu4e-compose-type) compose-type)
(put 'mu4e-compose-type 'permanent-local t)
;; bind to `mu4e-compose-parent-message' of compose buffer
(set (make-local-variable 'mu4e-compose-parent-message) original-msg)
(put 'mu4e-compose-parent-message 'permanent-local t)
;; set mu4e-compose-type once more for this buffer,
(set (make-local-variable 'mu4e-compose-type) compose-type)
(put 'mu4e-compose-type 'permanent-local t)
;; hide some headers
(mu4e~compose-hide-headers)
;; switch on the mode
(mu4e-compose-mode)
;; hide some headers
(mu4e~compose-hide-headers)
;; switch on the mode
(mu4e-compose-mode)
;; now jump to some useful positions, and start writing that mail!
(if (member compose-type '(new forward))
(message-goto-to)
;; otherwise, it depends...
(pcase message-cite-reply-position
((or 'above 'traditional) (message-goto-body))
(_ (when (message-goto-signature) (forward-line -2)))))
;; now jump to some useful positions, and start writing that mail!
(if (member compose-type '(new forward))
(message-goto-to)
;; otherwise, it depends...
(pcase message-cite-reply-position
((or 'above 'traditional) (message-goto-body))
(_ (when (message-goto-signature) (forward-line -2)))))
;; don't allow undoing anything before this.
(setq buffer-undo-list nil)
;; don't allow undoing anything before this.
(setq buffer-undo-list nil)
(when mu4e-compose-in-new-frame
;; make sure to close the frame when we're done with the message these are
;; all buffer-local;
(push 'delete-frame message-exit-actions)
(push 'delete-frame message-postpone-actions))
(when mu4e-compose-in-new-frame
;; make sure to close the frame when we're done with the message these are
;; all buffer-local;
(push 'delete-frame message-exit-actions)
(push 'delete-frame message-postpone-actions))
;; buffer is not user-modified yet
(set-buffer-modified-p nil))
;; buffer is not user-modified yet
(set-buffer-modified-p nil)
(mu4e-display-buffer draft-buffer t)))
(defun mu4e~switch-back-to-mu4e-buffer ()
"Try to go back to some previous buffer, in the order view->headers->main."
(unless (eq mu4e-split-view 'single-window)
(if (buffer-live-p (mu4e-get-view-buffer))
(switch-to-buffer (mu4e-get-view-buffer))
(if (buffer-live-p (mu4e-get-headers-buffer))
(switch-to-buffer (mu4e-get-headers-buffer))
;; if all else fails, back to the main view
(when (fboundp 'mu4e) (mu4e))))))
(if (buffer-live-p (mu4e-get-view-buffer))
(mu4e-display-buffer (mu4e-get-view-buffer) t)
(if (buffer-live-p (mu4e-get-headers-buffer))
(mu4e-display-buffer (mu4e-get-headers-buffer) t)
;; if all else fails, back to the main view
(when (fboundp 'mu4e) (mu4e)))))
(defun mu4e-compose-context-switch (&optional force name)
"Change the context for the current draft message.
@ -755,10 +756,7 @@ Symbol `edit' is only allowed for draft messages."
;; composing a new message, so that one will be replaced by the compose
;; window. The 10-or-so line headers buffer is not a good place to write
;; it...
(unless (eq mu4e-split-view 'single-window)
(let ((viewwin (get-buffer-window (mu4e-get-view-buffer))))
(when (window-live-p viewwin)
(select-window viewwin))))
;; (mu4e-display-buffer (mu4e-get-view-buffer))
;; talk to the backend
(mu4e--server-compose compose-type decrypt docid)))))
@ -798,8 +796,7 @@ draft message."
;;;###autoload
(defun mu4e~compose-mail (&optional to subject other-headers _continue
switch-function yank-action
_send-actions _return-action)
yank-action _send-actions _return-action)
"This is mu4e's implementation of `compose-mail'.
Quoting its docstring:
@ -815,9 +812,6 @@ HEADER and VALUE are strings.
CONTINUE, if non-nil, says to continue editing a message already
being composed. Interactively, CONTINUE is the prefix argument.
SWITCH-FUNCTION, if non-nil, is a function to use to
switch to and display the buffer used for mail composition.
YANK-ACTION, if non-nil, is an action to perform, if and when
necessary, to insert the raw text of the message being replied
to. It has the form (FUNCTION . ARGS). The user agent will apply
@ -834,11 +828,11 @@ called after the mail has been sent or put aside, and the mail
buffer buried."
(unless (mu4e-running-p)
(mu4e))
(mu4e))
;; create a new draft message 'resetting' (as below) is not actually needed in
;; this case, but let's prepare for the re-edit case as well
(mu4e~compose-handler 'new nil nil switch-function)
(mu4e~compose-handler 'new nil nil)
(when (message-goto-to) ;; reset to-address, if needed
(message-delete-line))

View File

@ -231,11 +231,6 @@ mu4e-specific version of `message-signature'."
:type 'boolean
:group 'mu4e-compose)
(defcustom mu4e-compose-in-new-frame nil
"Whether to compose messages in a new frame."
:type 'boolean
:group 'mu4e-compose)
(defvar mu4e-user-agent-string
(format "mu4e %s; emacs %s" mu4e-mu-version emacs-version)
"The User-Agent string for mu4e, or nil.")
@ -665,14 +660,9 @@ will be the same as in the original."
"The drafts-folder for this compose buffer.
This is based on `mu4e-drafts-folder', which is evaluated once.")
(defun mu4e~draft-open-file (path switch-function)
(defun mu4e~draft-open-file (path)
"Open the the draft file at PATH."
(let ((buf (find-file-noselect path)))
(funcall (or
switch-function
(and mu4e-compose-in-new-frame 'switch-to-buffer-other-frame)
'switch-to-buffer)
buf)))
(find-file-noselect path))
(defun mu4e~draft-determine-path (draft-dir)
@ -681,7 +671,7 @@ This is based on `mu4e-drafts-folder', which is evaluated once.")
(mu4e-root-maildir) draft-dir (mu4e~draft-message-filename-construct "DS")))
(defun mu4e-draft-open (compose-type &optional msg switch-function)
(defun mu4e-draft-open (compose-type &optional msg)
"Open a draft file for a message MSG.
In case of a new message (when COMPOSE-TYPE is `reply', `forward'
or `new'), open an existing draft (when COMPOSE-TYPE is `edit'),
@ -692,8 +682,11 @@ concatenation of `(mu4e-root-maildir)' and `mu4e-drafts-folder' (the
latter will be evaluated). The message file name is a unique name
determined by `mu4e-send-draft-file-name'. The initial contents
will be created from either `mu4e~draft-reply-construct', or
`mu4e~draft-forward-construct' or `mu4e~draft-newmsg-construct'."
(let ((draft-dir nil))
`mu4e~draft-forward-construct' or `mu4e~draft-newmsg-construct'.
Returns the newly-created draft buffer."
(let ((draft-dir nil)
(draft-buffer))
(cl-case compose-type
(edit
@ -701,7 +694,7 @@ will be created from either `mu4e~draft-reply-construct', or
;; full path, but we cannot really know 'drafts folder'... we make a
;; guess
(setq draft-dir (mu4e--guess-maildir (mu4e-message-field msg :path)))
(mu4e~draft-open-file (mu4e-message-field msg :path) switch-function))
(setq draft-buffer (mu4e~draft-open-file (mu4e-message-field msg :path))))
(resend
;; case-2: copy some exisisting message to a draft message, then edit
@ -709,7 +702,7 @@ will be created from either `mu4e~draft-reply-construct', or
(setq draft-dir (mu4e--guess-maildir (mu4e-message-field msg :path)))
(let ((draft-path (mu4e~draft-determine-path draft-dir)))
(copy-file (mu4e-message-field msg :path) draft-path)
(mu4e~draft-open-file draft-path switch-function)))
(setq draft-buffer (mu4e~draft-open-file draft-path))))
((reply forward new)
;; case-3: creating a new message; in this case, we can determine
@ -721,7 +714,8 @@ will be created from either `mu4e~draft-reply-construct', or
(reply (mu4e~draft-reply-construct msg))
(forward (mu4e~draft-forward-construct msg))
(new (mu4e~draft-newmsg-construct)))))
(mu4e~draft-open-file draft-path switch-function)
(setq draft-buffer (mu4e~draft-open-file draft-path))
(set-buffer draft-buffer)
(insert initial-contents)
(newline)
;; include the message signature (if it's set)
@ -739,7 +733,9 @@ will be created from either `mu4e~draft-reply-construct', or
(set (make-local-variable 'mu4e~draft-drafts-folder) draft-dir)
(put 'mu4e~draft-drafts-folder 'permanent-local t)
(unless mu4e~draft-drafts-folder
(mu4e-error "Failed to determine drafts folder"))))
(mu4e-error "Failed to determine drafts folder"))
;; return the name of the draft buffer
draft-buffer))
;;; _
(provide 'mu4e-draft)

View File

@ -104,18 +104,7 @@ In the format of `format-time-string'."
:type 'string
:group 'mu4e-headers)
(defcustom mu4e-headers-visible-lines 10
"Number of lines to display in the header view when using the
horizontal split-view. This includes the header-line at the top,
and the mode-line."
:type 'integer
:group 'mu4e-headers)
(defcustom mu4e-headers-visible-columns 30
"Number of columns to display for the header view when using the
vertical split-view."
:type 'integer
:group 'mu4e-headers)
(defcustom mu4e-headers-precise-alignment nil
"When set, use precise (but relatively slow) alignment for columns.
@ -727,9 +716,7 @@ docid is not found."
(defun mu4e~headers-view-this-message-p (docid)
"Is DOCID currently being viewed?"
(when (buffer-live-p (mu4e-get-view-buffer))
(with-current-buffer (mu4e-get-view-buffer)
(eq docid (plist-get mu4e~view-message :docid)))))
(mu4e-get-view-buffers (lambda (_) (eq docid (plist-get mu4e~view-message :docid)))))
;; note: this function is very performance-sensitive
(defun mu4e~headers-append-handler (msglst)
@ -811,10 +798,10 @@ present, don't do anything."
;; if we were viewing this message, close it now.
(when (and (mu4e~headers-view-this-message-p docid)
(buffer-live-p (mu4e-get-view-buffer)))
(unless (eq mu4e-split-view 'single-window)
(let ((buf (mu4e-get-view-buffer)))
(mapc #'delete-window (get-buffer-window-list
(mu4e-get-view-buffer) nil t)))
(kill-buffer (mu4e-get-view-buffer))))
buf nil t))
(kill-buffer buf))))
@ -828,7 +815,7 @@ present, don't do anything."
Switch to the output buffer for the results. If IGNORE-HISTORY is
true, do *not* update the query history stack."
(let* ((buf (get-buffer-create mu4e-headers-buffer-name))
(let* ((buf (mu4e-get-headers-buffer nil t))
(inhibit-read-only t)
(rewritten-expr (funcall mu4e-query-rewrite-function expr))
(maxnum (unless mu4e-search-full mu4e-search-results-limit)))
@ -845,7 +832,7 @@ true, do *not* update the query history stack."
;; when the buffer is already visible, select it; otherwise,
;; switch to it.
(unless (get-buffer-window buf 0)
(switch-to-buffer buf))
(mu4e-display-buffer buf t))
(run-hook-with-args 'mu4e-search-hook expr)
(mu4e~headers-clear mu4e~search-message)
(setq mu4e~headers-search-start (float-time))
@ -1158,7 +1145,8 @@ no user-interaction ongoing."
(when (and mu4e-headers-auto-update ;; must be set
mu4e-index-update-status
(not (zerop (plist-get mu4e-index-update-status :updated)))
(zerop (mu4e-mark-marks-num)) ;; non active marks
;; NOTE: `mu4e-mark-marks-num' can return nil. Is that intended?
(zerop (or (mu4e-mark-marks-num) 0)) ;; non active marks
(not (active-minibuffer-window))) ;; no user input only
;; rerun search if there's a live window with search results;
;; otherwise we'd trigger a headers view from out of nowhere.
@ -1332,32 +1320,6 @@ message plist, or nil if not found."
""))))))
(defun mu4e~headers-redraw-get-view-window ()
"Close all windows, redraw the headers buffer based on the value
of `mu4e-split-view', and return a window for the message view."
(if (eq mu4e-split-view 'single-window)
(or (and (buffer-live-p (mu4e-get-view-buffer))
(get-buffer-window (mu4e-get-view-buffer)))
(selected-window))
(mu4e-hide-other-mu4e-buffers)
(unless (buffer-live-p (mu4e-get-headers-buffer))
(mu4e-error "No headers buffer available"))
(switch-to-buffer (mu4e-get-headers-buffer))
;; kill the existing view buffer
(when (buffer-live-p (mu4e-get-view-buffer))
(kill-buffer (mu4e-get-view-buffer)))
;; get a new view window
(setq mu4e~headers-view-win
(with-demoted-errors "Unable to split window: %S"
(cond
((eq mu4e-split-view 'horizontal) ;; split horizontally
(split-window-vertically mu4e-headers-visible-lines))
((eq mu4e-split-view 'vertical) ;; split vertically
(split-window-horizontally mu4e-headers-visible-columns))
((functionp mu4e-split-view)
(funcall mu4e-split-view))
(t ;; no splitting; just use the currently selected one
(selected-window)))))))
;;; Search-based marking
@ -1627,41 +1589,24 @@ _not_ refresh the last search with the new setting for threading."
(mu4e~headers-toggle "Skip-duplicates"
'mu4e-headers-skip-duplicates dont-refresh))
(defvar mu4e~headers-loading-buf nil
"A buffer for loading a message view.")
(defun mu4e-headers-view-message ()
"View message at point .
If there's an existing window for the view, re-use that one . If
not, create a new one, depending on the value of
`mu4e-split-view': if it's a symbol `horizontal' or `vertical',
split the window accordingly; if it is nil, replace the current
window . "
"View message at point."
(interactive)
(unless (eq major-mode 'mu4e-headers-mode)
(mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode))
(let* ((msg (mu4e-message-at-point))
(path (mu4e-message-field msg :path))
(_exists (or (file-readable-p path)
(mu4e-warn "No message at %s" path)))
(mu4e-warn "No message at %s" path)))
(docid (or (mu4e-message-field msg :docid)
(mu4e-warn "No message at point")))
(mark-as-read
(if (functionp mu4e-view-auto-mark-as-read)
(funcall mu4e-view-auto-mark-as-read msg)
mu4e-view-auto-mark-as-read))
(viewwin (mu4e~headers-redraw-get-view-window)))
(unless (window-live-p viewwin)
(mu4e-error "Cannot get a message view"))
(select-window viewwin)
;; show some 'loading...' buffer
(unless (buffer-live-p mu4e~headers-loading-buf)
(setq mu4e~headers-loading-buf (get-buffer-create " *mu4e-loading*"))
(with-current-buffer mu4e~headers-loading-buf
(mu4e-loading-mode)))
(switch-to-buffer mu4e~headers-loading-buf)
mu4e-view-auto-mark-as-read)))
(when-let ((buf (mu4e-get-view-buffer (current-buffer) nil)))
(with-current-buffer buf
(mu4e-loading-mode 1)))
(mu4e--server-view docid mark-as-read)))
@ -1692,15 +1637,15 @@ return nil."
;; update all windows showing the headers buffer
(walk-windows
(lambda (win)
(when (eq (window-buffer win) (mu4e-get-headers-buffer))
(when (eq (window-buffer win) (mu4e-get-headers-buffer (buffer-name)))
(set-window-point win (point))))
nil t)
(if (eq mu4e-split-view 'single-window)
(when (eq (window-buffer) (mu4e-get-view-buffer))
(mu4e-headers-view-message))
;; update message view if it was already showing
(when (and mu4e-split-view (window-live-p mu4e~headers-view-win))
(mu4e-headers-view-message)))
;; If the assigned (and buffer-local) `mu4e~headers-view-win'
;; is not live then that is indicates the user does not want
;; to pop up the view when they navigate in the headers
;; buffer.
(when (window-live-p mu4e~headers-view-win)
(mu4e-headers-view-message))
;; attempt to highlight the new line, display the message
(mu4e~headers-highlight docid)
(if succeeded
@ -1811,58 +1756,42 @@ region if there is a region, then move to the next message."
This is a rather complex function, to ensure we don't disturb
other windows."
(interactive)
(if (eq mu4e-split-view 'single-window)
(progn (mu4e-mark-handle-when-leaving)
(kill-buffer))
(unless (eq major-mode 'mu4e-headers-mode)
(mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode))
(mu4e-mark-handle-when-leaving)
(let ((curbuf (current-buffer))
(curwin (selected-window)))
(walk-windows
(lambda (win)
(with-selected-window win
;; if we the view window connected to this one, kill it
(when (and (not (one-window-p win)) (eq mu4e~headers-view-win win))
(delete-window win)
(setq mu4e~headers-view-win nil)))
;; and kill any _other_ (non-selected) window that shows the current
;; buffer
(when (and
(eq curbuf (window-buffer win)) ;; does win show curbuf?
(not (eq curwin win)) ;; it's not the curwin?
(not (one-window-p))) ;; and not the last one?
(delete-window win)))) ;; delete it!
;; now, all *other* windows should be gone. kill ourselves, and return
;; to the main view
(kill-buffer)
(mu4e--main-view 'refresh))))
(mu4e-mark-handle-when-leaving)
(quit-window t)
(mu4e--main-view 'refresh))
;;; Loading messages
;;
(defvar mu4e-loading-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "n" #'ignore)
(define-key map "p" #'ignore)
(define-key map "q" #'bury-buffer)
map)
"Keymap for *mu4e-loading* buffers.")
(define-derived-mode mu4e-loading-mode special-mode
"mu4e:loading"
(use-local-map mu4e-loading-mode-map)
(let ((inhibit-read-only t))
(erase-buffer)
(insert (propertize "Loading message..."
'face 'mu4e-system-face 'intangible t))))
(defun mu4e~loading-close ()
"Bury the mu4e Loading... buffer, if any."
(let* ((buf mu4e~headers-loading-buf)
(win (and (buffer-live-p buf) (get-buffer-window buf t))))
(when (window-live-p win)
(delete-window win))))
(defvar-local mu4e--loading-overlay-bg nil
"Internal variable that holds the loading overlay for the background.")
(defvar-local mu4e--loading-overlay-text nil
"Internal variable that holds the loading overlay for the text.")
(define-minor-mode mu4e-loading-mode
"Minor mode for buffers awaiting data from mu"
:init-value nil :lighter " Loading" :keymap nil
(if mu4e-loading-mode
(progn
(when mu4e-dim-when-loading
(setq mu4e--loading-overlay-bg
(let ((overlay (make-overlay (point-min) (point-max))))
(overlay-put overlay 'face `(:foreground "gray22" :background
,(face-attribute 'default :background)))
(overlay-put overlay 'priority 9998)
overlay)))
(setq mu4e--loading-overlay-text
(let ((overlay (make-overlay (point-min) (point-min))))
(overlay-put overlay 'priority 9999)
(overlay-put overlay 'before-string (propertize "Loading…\n" 'face 'mu4e-header-title-face))
overlay)))
(when mu4e--loading-overlay-bg
(delete-overlay mu4e--loading-overlay-bg))
(when mu4e--loading-overlay-text
(delete-overlay mu4e--loading-overlay-text))))
(provide 'mu4e-headers)
;;; mu4e-headers.el ends here

View File

@ -32,6 +32,7 @@
(require 'cl-lib)
(require 'bookmark)
(require 'mu4e-window)
(require 'mu4e-config)
;;; Customization
@ -86,79 +87,26 @@ marked as read-only, or non-nil otherwise."
:group 'mu4e-view)
(defcustom mu4e-split-view 'horizontal
"How to show messages / headers.
A symbol which is either:
* `horizontal': split horizontally (headers on top)
* `vertical': split vertically (headers on the left).
* `single-window': view and headers in one window (mu4e will try not to
touch your window layout), main view in minibuffer
* a function: the function is responsible to return some window for
the view.
* anything else: don't split (show either headers or messages,
not both).
Also see `mu4e-headers-visible-lines'
and `mu4e-headers-visible-columns'."
:type '(choice (const :tag "Split horizontally" horizontal)
(const :tag "Split vertically" vertical)
(const :tag "Single window" single-window)
(const :tag "Don't split" nil))
:group 'mu4e-headers)
;;; Buffers
(defconst mu4e-main-buffer-name " *mu4e-main*"
"Name of the mu4e main buffer.
The default name starts with SPC and therefore is not visible in
buffer list.")
(defconst mu4e-headers-buffer-name "*mu4e-headers*"
"Name of the buffer for message headers.")
(defconst mu4e-embedded-buffer-name " *mu4e-embedded*"
"Name for the embedded message view buffer.")
(defconst mu4e-view-buffer-name "*Article*"
"Name of the view buffer.")
(defun mu4e-get-headers-buffer ()
"Get the buffer object from `mu4e-headers-buffer-name'."
(get-buffer mu4e-headers-buffer-name))
(defun mu4e-get-view-buffer ()
"Get the buffer object from `mu4e-view-buffer-name'."
(get-buffer mu4e-view-buffer-name))
(defun mu4e-select-other-view ()
"Switch between headers view and message view."
(interactive)
(let* ((other-buf
(cond
((eq major-mode 'mu4e-headers-mode)
((mu4e-current-buffer-type-p 'view)
(mu4e-get-headers-buffer))
((mu4e-current-buffer-type-p 'headers)
(mu4e-get-view-buffer))
((eq major-mode 'mu4e-view-mode)
(mu4e-get-headers-buffer))))
(t (mu4e-error "This window is neither the headers nor the view window."))))
(other-win (and other-buf (get-buffer-window other-buf))))
(if (window-live-p other-win)
(select-window other-win)
(mu4e-message "No window to switch to"))))
;;; Windows
(defun mu4e-hide-other-mu4e-buffers ()
"Bury mu4e buffers.
Hide (main, headers, view) (and delete all windows displaying
it). Do _not_ bury the current buffer, though."
(interactive)
(unless (eq mu4e-split-view 'single-window)
(let ((curbuf (current-buffer)))
;; note: 'walk-windows' does not seem to work correctly when modifying
;; windows; therefore, the doloops here
(dolist (frame (frame-list))
(dolist (win (window-list frame nil))
(with-current-buffer (window-buffer win)
(unless (eq curbuf (current-buffer))
(when (member major-mode '(mu4e-headers-mode mu4e-view-mode))
(when (eq t (window-deletable-p win))
(delete-window win))))))) t)))
;;; Modeline
(defun mu4e-quote-for-modeline (str)
@ -199,8 +147,6 @@ Create [mu4e]-prefixed error based on format FRM and ARGS. Does a
local-exit and does not return, and raises a
debuggable (backtrace) error."
(mu4e-log 'error (apply 'mu4e-format frm args))
;; opportunistically close the "loading" window.
(mu4e~loading-close)
(error "%s" (apply 'mu4e-format frm args)))
(defun mu4e-warn (frm &rest args)
@ -406,7 +352,7 @@ log-buffer. See `mu4e-show-log'."
(let ((buf (get-buffer mu4e--log-buffer-name)))
(unless (buffer-live-p buf)
(mu4e-warn "No debug log available"))
(switch-to-buffer buf)))
(display-buffer buf)))

View File

@ -191,7 +191,7 @@
(funcall action docid original-msg target))
(when (and (mu4e~headers-view-this-message-p docid)
(buffer-live-p (mu4e-get-view-buffer)))
(switch-to-buffer (mu4e-get-view-buffer))
(mu4e-display-buffer (mu4e-get-view-buffer))
(or (mu4e-view-headers-next)
(kill-buffer-and-window))))))

View File

@ -33,6 +33,7 @@
(require 'mu4e-contacts)
(require 'mu4e-search)
(require 'mu4e-vars) ;; mu-wide variables
(require 'mu4e-window)
(declare-function mu4e-compose-new "mu4e-compose")
(declare-function mu4e~headers-jump-to-maildir "mu4e-headers")
@ -371,17 +372,13 @@ When REFRESH is non nil refresh infos from server."
When REFRESH is non nil refresh infos from server."
(let ((buf (get-buffer-create mu4e-main-buffer-name)))
(if (eq mu4e-split-view 'single-window)
(if (buffer-live-p (mu4e-get-headers-buffer))
(switch-to-buffer (mu4e-get-headers-buffer))
(mu4e--main-menu))
;; `mu4e--main-view' is called from `mu4e--start', so don't call it
;; a second time here i.e. do not refresh unless specified
;; explicitly with REFRESH arg.
(switch-to-buffer buf)
(with-current-buffer buf
(mu4e--main-view-real-1 refresh))
(goto-char (point-min)))))
;; `mu4e--main-view' is called from `mu4e--start', so don't call it
;; a second time here i.e. do not refresh unless specified
;; explicitly with REFRESH arg.
(mu4e-display-buffer buf t)
(with-current-buffer buf
(mu4e--main-view-real-1 refresh))
(goto-char (point-min))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Interactive functions
@ -420,8 +417,9 @@ When REFRESH is non nil refresh infos from server."
(defun mu4e--main-update-after-index ()
"Update the main view buffer after indexing."
(with-current-buffer mu4e-main-buffer-name
(revert-buffer)))
(when (buffer-live-p mu4e-main-buffer-name)
(with-current-buffer mu4e-main-buffer-name
(revert-buffer))))
(provide 'mu4e-main)
;;; mu4e-main.el ends here

View File

@ -98,31 +98,23 @@ is the target directory (for \"move\")")
(defun mu4e--mark-find-headers-buffer ()
"Find the headers buffer, if any."
(seq-find (lambda (b)
(with-current-buffer b
(eq major-mode 'mu4e-headers-mode)))
(seq-find (lambda (_)
(mu4e-current-buffer-type-p 'headers))
(buffer-list)))
(defmacro mu4e--mark-in-context (&rest body)
"Evaluate BODY in the context of the headers buffer.
The current buffer must be either a headers or view buffer."
`(cond
((eq major-mode 'mu4e-headers-mode) ,@body)
((eq major-mode 'mu4e-view-mode)
((mu4e-current-buffer-type-p 'headers) ,@body)
((mu4e-current-buffer-type-p 'view)
(when (buffer-live-p (mu4e-get-headers-buffer))
(let* ((msg (mu4e-message-at-point))
(docid (mu4e-message-field msg :docid)))
(with-current-buffer (mu4e-get-headers-buffer)
(if (mu4e~headers-goto-docid docid)
,@body
(mu4e-error "Cannot find message in headers buffer"))))))
(t
;; even in other modes (e.g. mu4e-main-mode we try to find
;; the headers buffer
(let ((hbuf (mu4e--mark-find-headers-buffer)))
(if (buffer-live-p hbuf)
(with-current-buffer hbuf ,@body)
,@body)))))
(when (mu4e~headers-goto-docid docid)
,@body
)))))))
(defconst mu4e-marks
'((refile

View File

@ -28,6 +28,7 @@
(require 'mu4e-vars)
(require 'mu4e-contacts)
(require 'mu4e-window)
(require 'flow-fill)
(require 'shr)
(require 'pp)
@ -206,18 +207,20 @@ If MSG is nil, use `mu4e-message-at-point'."
(kill-new path)
(mu4e-message "Saved '%s' to kill-ring" path)))
(defconst mu4e--sexp-buffer-name " *mu4e-sexp-at-point"
"Buffer name for sexp buffers.")
(defun mu4e-sexp-at-point ()
"Show or hide the s-expression for the message-at-point, if any."
(interactive)
(if-let ((win (get-buffer-window mu4e--sexp-buffer-name)))
(delete-window win)
(when-let ((msg (mu4e-message-at-point 'noerror)))
(with-current-buffer-window mu4e--sexp-buffer-name nil nil
;; the "pretty-printing" is not very pretty...
(insert (pp-to-string msg))))))
(when (buffer-live-p mu4e--sexp-buffer-name)
(kill-buffer mu4e--sexp-buffer-name))
(with-current-buffer-window (get-buffer-create mu4e--sexp-buffer-name) nil nil
(lisp-data-mode)
(insert (pp-to-string msg))
(font-lock-ensure)
;; add basic `quit-window' bindings
(view-mode 1)))))
;;;
(provide 'mu4e-message)

View File

@ -211,21 +211,22 @@ if you otherwise want to use `mu4e-index-lazy-check'."
(defvar mu4e--update-buffer nil
"The buffer of the update process when updating.")
(define-derived-mode mu4e--update-mail-mode
special-mode "mu4e:update"
(define-derived-mode mu4e--update-mail-mode special-mode "mu4e:update"
"Major mode used for retrieving new e-mail messages in `mu4e'.")
(define-key mu4e--update-mail-mode-map (kbd "q") 'mu4e-kill-update-mail)
(defun mu4e--temp-window (buf height)
"Create a temporary window with HEIGHT at the bottom BUF."
(let ((win
(split-window
(frame-root-window)
(- (window-height (frame-root-window)) height))))
(set-window-buffer win buf)
(set-window-dedicated-p win t)
win))
"Create a temporary window with HEIGHT at the bottom BUF.
This function uses `display-buffer' with a default preset.
To override this behavior, customize `display-buffer-alist'."
(display-buffer buf `(display-buffer-at-bottom
(preserve-size . (nil . t))
(height . ,height)
(window-height . fit-window-to-buffer)))
(set-window-buffer (get-buffer-window buf) buf))
(defun mu4e--update-sentinel-func (proc _msg)
"Sentinel function for the update process PROC."
@ -244,8 +245,7 @@ special-mode "mu4e:update"
(mu4e-update-index)))
(mu4e-update-index))
(when (buffer-live-p mu4e--update-buffer)
(unless (eq mu4e-split-view 'single-window)
(mapc #'delete-window (get-buffer-window-list mu4e--update-buffer)))
(delete-windows-on mu4e--update-buffer t)
(kill-buffer mu4e--update-buffer)))
;; complicated function, as it:
@ -268,8 +268,6 @@ run in the background; otherwise, pop up a window."
(setq mu4e--update-buffer buf)
(when (window-live-p win)
(with-selected-window win
;; ;;(switch-to-buffer buf)
;; (set-window-dedicated-p win t)
(erase-buffer)
(insert "\n") ;; FIXME -- needed so output starts
(mu4e--update-mail-mode)))

View File

@ -56,6 +56,12 @@ Follows the format of `format-time-string'."
:type 'string
:group 'mu4e)
(defcustom mu4e-dim-when-loading t
"Dim buffer text when loading new data.
If non-nil, dim some buffers during data retrieval and rendering."
:type 'boolean
:group 'mu4e)
;;; Faces
@ -355,7 +361,7 @@ header-view, not including, for instance, the message body.")
;;; Internals
(defvar mu4e~headers-view-win nil
(defvar-local mu4e~headers-view-win nil
"The view window connected to this headers view.")
;; It's useful to have the current view message available to
@ -363,7 +369,7 @@ header-view, not including, for instance, the message body.")
;; before calling `mu4e-view-mode'. However, changing the major mode
;; clobbers any local variables. Work around that by declaring the
;; variable permanent-local.
(defvar mu4e~view-message nil "The message being viewed in view mode.")
(defvar-local mu4e~view-message nil "The message being viewed in view mode.")
(put 'mu4e~view-message 'permanent-local t)
;;; _
(provide 'mu4e-vars)

View File

@ -112,6 +112,10 @@ specified a function as viewer."
:type 'integer
:group 'mu4e-view)
(defcustom mu4e-after-view-message-hook '(mu4e-resize-linked-headers-window)
"Hook run by `mu4e-view' after a message is rendered."
:type 'hook
:group 'mu4e-view)
@ -122,42 +126,7 @@ specified a function as viewer."
This is a rather complex function, to ensure we don't disturb
other windows."
(interactive)
(if (eq mu4e-split-view 'single-window)
(when (buffer-live-p (mu4e-get-view-buffer))
(kill-buffer (mu4e-get-view-buffer)))
(unless (eq major-mode 'mu4e-view-mode)
(mu4e-error "Must be in mu4e-view-mode (%S)" major-mode))
(let ((curbuf (current-buffer))
(curwin (selected-window))
(headers-win))
(walk-windows
(lambda (win)
;; check whether the headers buffer window is visible
(when (eq (mu4e-get-headers-buffer) (window-buffer win))
(setq headers-win win))
;; and kill any _other_ (non-selected) window that shows the current
;; buffer
(when
(and
(eq curbuf (window-buffer win)) ;; does win show curbuf?
(not (eq curwin win)) ;; but it's not the curwin?
(not (one-window-p))) ;; and not the last one on the frame?
(delete-window win)))) ;; delete it!
;; now, all *other* windows should be gone.
;; if the headers view is also visible, kill ourselves + window; otherwise
;; switch to the headers view
(if (window-live-p headers-win)
;; headers are visible
(progn
(kill-buffer-and-window) ;; kill the view win
(setq mu4e~headers-view-win nil)
(select-window headers-win)) ;; and switch to the headers win...
;; headers are not visible...
(progn
(kill-buffer)
(setq mu4e~headers-view-win nil)
(when (buffer-live-p (mu4e-get-headers-buffer))
(switch-to-buffer (mu4e-get-headers-buffer))))))))
(quit-window t))
(defconst mu4e~view-raw-buffer-name " *mu4e-raw-view*"
@ -171,10 +140,10 @@ other windows."
(with-current-buffer buf
(let ((inhibit-read-only t))
(erase-buffer)
(insert-file-contents path)
(view-mode)
(mu4e-raw-view-mode)
(insert-file-contents path)
(goto-char (point-min))))
(switch-to-buffer buf)))
(mu4e-display-buffer buf t)))
(defun mu4e-view-pipe (cmd)
"Pipe the message at point through shell command CMD.
@ -186,17 +155,36 @@ Then, display the results."
(defmacro mu4e~view-in-headers-context (&rest body)
"Evaluate BODY in the context of the headers buffer."
`(progn
(unless (buffer-live-p (mu4e-get-headers-buffer))
(mu4e-error "No headers buffer connected"))
(let* ((msg (mu4e-message-at-point))
(buffer (cond
;; are we already inside a headers buffer?
((mu4e-current-buffer-type-p 'headers) (current-buffer))
;; if not, are we inside a view buffer, and does
;; it have linked headers buffer?
((mu4e-current-buffer-type-p 'view)
(when (mu4e--view-detached-p (current-buffer))
(mu4e-error "You cannot navigate in a detached view buffer."))
(mu4e-get-headers-buffer))
;; fallback; but what would trigger this?
(t (mu4e-get-headers-buffer))))
(docid (mu4e-message-field msg :docid)))
(unless docid
(mu4e-error "Message without docid: action is not possible"))
(with-current-buffer (mu4e-get-headers-buffer)
(unless (eq mu4e-split-view 'single-window)
(when (get-buffer-window)
(select-window (get-buffer-window))))
(if (mu4e~headers-goto-docid docid)
(with-current-buffer buffer
(mu4e-display-buffer buffer)
(if (or (mu4e~headers-goto-docid docid)
;; TODO: Is this the best way to find another
;; relevant docid for a view buffer?
;;
;; If you attach a view buffer to another headers
;; buffer that does not contain the current docid
;; then `mu4e~headers-goto-docid' returns nil and we
;; get an error. This "hack" instead gets its
;; now-changed headers buffer's current message as a
;; docid
(mu4e~headers-goto-docid
(with-current-buffer buffer
(mu4e-message-field (mu4e-message-at-point) :docid))))
,@body
(mu4e-error "Cannot find message in headers buffer"))))))
@ -226,12 +214,8 @@ message view. If this succeeds, return the new docid. Otherwise,
return nil."
(mu4e~view-in-headers-context
(mu4e~headers-prev-or-next-unread backwards))
(if (eq mu4e-split-view 'single-window)
(when (eq (window-buffer) (mu4e-get-view-buffer))
(with-current-buffer (mu4e-get-headers-buffer)
(mu4e-headers-view-message)))
(mu4e-select-other-view)
(mu4e-headers-view-message)))
(mu4e-select-other-view)
(mu4e-headers-view-message))
(defun mu4e-view-headers-prev-unread ()
"Move point to the previous unread message header.
@ -336,6 +320,31 @@ Add this function to `mu4e-view-mode-hook' to enable this feature."
"Return t if we're in split-view, nil otherwise."
(member mu4e-split-view '(horizontal vertical)))
(defun mu4e-view-detach ()
"Detach the view buffer from its headers buffer."
(interactive)
(unless mu4e-linked-headers-buffer
(mu4e-error "This view buffer is already detached."))
(mu4e-message "Detached view buffer from %s"
(prog1 mu4e-linked-headers-buffer
(with-current-buffer mu4e-linked-headers-buffer
(when (eq (selected-window) mu4e~headers-view-win)
(setq mu4e~headers-view-win nil)))
(setq mu4e-linked-headers-buffer nil))))
(defun mu4e-view-attach (headers-buffer)
"Attaches a view buffer to a headers buffer."
(interactive
(list (get-buffer (read-buffer
"Select a headers buffer to attach to: " nil t
(lambda (buf) (with-current-buffer (car buf)
(mu4e-current-buffer-type-p 'headers)))))))
(mu4e-message "Attached view buffer to %s" headers-buffer)
(setq mu4e-linked-headers-buffer headers-buffer)
(with-current-buffer headers-buffer
(setq mu4e~headers-view-win (selected-window))))
;;; Scroll commands
(defun mu4e-view-scroll-up-or-next ()
@ -599,27 +608,54 @@ in the the message view affects HDRSBUF, as does marking etc.
As a side-effect, a message that is being viewed loses its
`unread' marking if it still had that."
(mu4e~headers-update-handler msg nil nil) ;; update headers, if necessary.
(when (bufferp gnus-article-buffer)
(kill-buffer gnus-article-buffer))
(setq gnus-article-buffer mu4e-view-buffer-name)
(with-current-buffer (get-buffer-create gnus-article-buffer)
(let ((inhibit-read-only t))
(remove-overlays (point-min)(point-max) 'mu4e-overlay t)
(erase-buffer)
(insert-file-contents-literally
(mu4e-message-readable-path msg) nil nil nil t)))
(switch-to-buffer gnus-article-buffer)
(setq mu4e~view-message msg)
(mu4e~view-render-buffer msg))
;; update headers, if necessary.
(mu4e~headers-update-handler msg nil nil)
;; Create a new view buffer (if needed) as it is not
;; feasible to recycle an existing buffer due to buffer-specific
;; state (buttons, etc.) that can interfere with message rendering
;; in gnus.
;;
;; Unfortunately that does create its own issues: namely ensuring
;; buffer-local state that *must* survive is correctly copied
;; across.
(let ((linked-headers-buffer))
(when-let ((existing-buffer (mu4e-get-view-buffer nil nil)))
;; required; this state must carry over from the killed buffer
;; to the new one.
(setq linked-headers-buffer mu4e-linked-headers-buffer)
(delete-windows-on existing-buffer t)
(kill-buffer existing-buffer))
(setq gnus-article-buffer (mu4e-get-view-buffer nil t))
(with-current-buffer gnus-article-buffer
(when linked-headers-buffer
(setq mu4e-linked-headers-buffer linked-headers-buffer))
(let ((inhibit-read-only t))
(remove-overlays (point-min)(point-max) 'mu4e-overlay t)
(erase-buffer)
(insert-file-contents-literally
(mu4e-message-readable-path msg) nil nil nil t)
(setq-local mu4e~view-message msg)
(mu4e~view-render-buffer msg))
(mu4e-loading-mode 0)))
(unless (mu4e--view-detached-p gnus-article-buffer)
(with-current-buffer mu4e-linked-headers-buffer
;; We need this here as we want to avoid displaying the buffer until
;; the last possible moment --- after the message is rendered in the
;; view buffer.
;;
;; Otherwise, `mu4e-display-buffer' may adjust the view buffer's
;; window height based on a buffer that has no text in it yet!
(setq-local mu4e~headers-view-win (mu4e-display-buffer gnus-article-buffer nil))
(unless (window-live-p mu4e~headers-view-win)
(mu4e-error "Cannot get a message view"))
(select-window mu4e~headers-view-win)))
(with-current-buffer gnus-article-buffer
(run-hooks 'mu4e-after-view-message-hook)))
(defun mu4e-view-message-text (msg)
"Return the pristine MSG as a string."
;; we need this for replying/forwarding, since the mu4e-compose
;; wants it that way.
(with-temp-buffer
(insert-file-contents-literally
(mu4e-message-readable-path msg) nil nil nil t)
@ -686,7 +722,6 @@ determine which browser function to use."
(condition-case err
(progn
(mm-enable-multibyte)
(mu4e-view-mode)
(run-hooks 'gnus-article-decode-hook)
(gnus-article-prepare-display)
(mu4e~view-activate-urls)
@ -826,9 +861,8 @@ This is useful for advising some Gnus-functionality that does not work in mu4e."
(define-key map "q" #'mu4e~view-quit-buffer)
;; note, 'z' is by-default bound to 'bury-buffer'
;; but that's not very useful in this case
(define-key map "z" #'ignore)
(define-key map "z" #'mu4e-view-detach)
(define-key map "Z" #'mu4e-view-attach)
(define-key map "%" #'mu4e-view-mark-pattern)
(define-key map "t" #'mu4e-view-mark-subthread)
@ -985,12 +1019,21 @@ This is useful for advising some Gnus-functionality that does not work in mu4e."
(set-keymap-parent mu4e-view-mode-map button-buffer-map)
(defcustom mu4e-raw-view-mode-hook nil
"Hook run when entering \\[mu4e-raw-view] mode."
:options '()
:type 'hook
:group 'mu4e-view)
(defcustom mu4e-view-mode-hook nil
"Hook run when entering Mu4e-View mode."
"Hook run when entering \\[mu4e-view] mode."
:options '(turn-on-visual-line-mode)
:type 'hook
:group 'mu4e-view)
(define-derived-mode mu4e-raw-view-mode fundamental-mode "mu4e:raw-view"
(view-mode))
;; "Define the major-mode for the mu4e-view."
(define-derived-mode mu4e-view-mode gnus-article-mode "mu4e:view"
"Major mode for viewing an e-mail message in mu4e.
@ -1150,11 +1193,11 @@ containing commas."
(:name "raw" :handler (lambda (str)
(let ((tmpbuf
(get-buffer-create " *mu4e-raw-mime*")))
(with-current-buffer tmpbuf
(insert str)
(view-mode)
(goto-char (point-min)))
(switch-to-buffer tmpbuf))) :receives pipe))
(with-current-buffer tmpbuf
(insert str)
(view-mode)
(goto-char (point-min)))
(display-buffer tmpbuf))) :receives pipe))
"Specifies actions for MIME-parts.
@ -1274,7 +1317,7 @@ the third MIME-part."
(erase-buffer)
(call-process-shell-command pipecmd path t t)
(view-mode)))
(switch-to-buffer buf)))
(display-buffer buf)))
;;; Bug Reference mode support

308
mu4e/mu4e-window.el Normal file
View File

@ -0,0 +1,308 @@
;;; mu4e-window.el --- window management for mu4e -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Mickey Petersen
;; Author: Mickey Petersen <mickey@masteringemacs.org>
;; Keywords: mail
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
;;; Buffer names for internal use
(defconst mu4e--sexp-buffer-name "*mu4e-sexp-at-point*"
"Buffer name for sexp buffers.")
(defvar mu4e-main-buffer-name " *mu4e-main*"
"Name of the mu4e main buffer.
The default name starts with SPC and therefore is not visible in
buffer list.")
(defvar mu4e-embedded-buffer-name " *mu4e-embedded*"
"Name for the embedded message view buffer.")
;; Buffer names for public use
(defvar mu4e-headers-buffer-name "*mu4e-headers*"
"Name of the buffer for message headers.")
(defvar mu4e-view-buffer-name "*mu4e-article*"
"Name of the view buffer.")
(defvar mu4e-headers-buffer-name-func nil
"Function used to name the headers buffers.")
(defvar mu4e-view-buffer-name-func nil
"Function used to name the view buffers.
The function is given one argument, the headers buffer it is
linked to.")
(defvar-local mu4e-linked-headers-buffer nil
"Holds the headers buffer object that ties it to a view.")
(defcustom mu4e-split-view 'horizontal
"How to show messages / headers.
A symbol which is either:
* `horizontal': split horizontally (headers on top)
* `vertical': split vertically (headers on the left).
* `single-window': view and headers in one window (mu4e will try not to
touch your window layout), main view in minibuffer
* a function: the function is responsible to return some window for
the view.
* anything else: don't split (show either headers or messages,
not both).
Also see `mu4e-headers-visible-lines'
and `mu4e-headers-visible-columns'."
:type '(choice (const :tag "Split horizontally" horizontal)
(const :tag "Split vertically" vertical)
(const :tag "Single window" single-window)
(const :tag "Don't split" nil))
:group 'mu4e-headers)
(defcustom mu4e-compose-in-new-frame nil
"Whether to compose messages in a new frame."
:type 'boolean
:group 'mu4e-compose)
(defcustom mu4e-headers-visible-lines 10
"Number of lines to display in the header view when using the
horizontal split-view. This includes the header-line at the top,
and the mode-line."
:type 'integer
:group 'mu4e-headers)
(defcustom mu4e-headers-visible-columns 30
"Number of columns to display for the header view when using the
vertical split-view."
:type 'integer
:group 'mu4e-headers)
(declare-function mu4e-view-mode "mu4e-view")
(defun mu4e-get-headers-buffer (&optional buffer-name create)
"Return a related headers buffer optionally named BUFFER-NAME.
If CREATE is non-nil, the headers buffer is created if the
generated name does not already exist."
(let* ((buffer-name
(or
;; buffer name generator func. If a user wants
;; to supply its own naming scheme, we use that
;; in lieu of our own heuristic.
(and mu4e-headers-buffer-name-func
(funcall mu4e-headers-buffer-name-func))
;; if we're supplied a buffer name for a
;; headers buffer then try to use that one.
buffer-name
;; if we're asking for a headers buffer from a
;; view, then we get our linked buffer. If
;; there is no such linked buffer -- it is
;; detached -- raise an error.
(and (mu4e-current-buffer-type-p 'view)
mu4e-linked-headers-buffer)
;; if we're already in a headers buffer then
;; that is the one we use.
(and (mu4e-current-buffer-type-p 'headers)
(current-buffer))
;; default name to use if all other checks fail.
mu4e-headers-buffer-name))
(buffer (get-buffer buffer-name)))
(when (and (not (buffer-live-p buffer)) create)
(setq buffer (get-buffer-create buffer-name)))
;; This may conceivably return a non-existent buffer if `create'
;; and `buffer-live-p' are nil.
;;
;; This is seemingly "OK" as various parts of the code check for
;; buffer liveness themselves.
buffer))
(defun mu4e-get-view-buffers (pred)
"Filter all known view buffers and keep those where PRED return non-nil.
The PRED function is called from inside the buffer that is being
tested."
(seq-filter
(lambda (buf)
(with-current-buffer buf
(and (mu4e-current-buffer-type-p 'view)
(and pred (funcall pred buf)))))
(buffer-list)))
(defun mu4e--view-detached-p (buffer)
"Return non-nil if BUFFER is a detached view buffer."
(with-current-buffer buffer
(unless (mu4e-current-buffer-type-p 'view)
(error "Buffer `%s' is not a valid mu4e view buffer" buffer))
(null mu4e-linked-headers-buffer)))
(defun mu4e--get-current-buffer-type ()
"Return an internal symbol that corresponds to each mu4e major mode."
(cond ((derived-mode-p 'mu4e-view-mode 'mu4e-raw-view-mode) 'view)
((derived-mode-p 'mu4e-headers-mode) 'headers)
((derived-mode-p 'mu4e-compose-mode) 'compose)
((derived-mode-p 'mu4e-main-mode) 'main)
(t 'unknown)))
(defun mu4e-current-buffer-type-p (type)
"Return non-nil if the current buffer is a mu4e buffer of TYPE.
Where TYPE is `view', `headers', `compose', `main' or `unknown'.
Checks are performed using `derived-mode-p' and the current
buffer's major mode."
(eq (mu4e--get-current-buffer-type) type))
(defun mu4e-get-view-buffer (&optional headers-buffer create)
"Return a view buffer belonging optionally to HEADERS-BUFFER.
If HEADERS-BUFFER is nil, the most likely (and available) headers
buffer is used.
Detached view buffers are ignored; that may result in a new view buffer
being created if CREATE is non-nil."
;; If `headers-buffer' is nil, then the caller does not have a
;; headers buffer preference.
;;
;; In that case, we request the most plausible headers buffer from
;; `mu4e-get-headers-buffer'.
(when (setq headers-buffer (or headers-buffer (mu4e-get-headers-buffer)))
(let ((buffer)
;; If `mu4e-view-buffer-name-func' is non-nil, then use that
;; to source the name of the view buffer to create or re-use.
(buffer-name (or (and mu4e-view-buffer-name-func
(funcall mu4e-view-buffer-name-func headers-buffer))
;; If the variable is nil, use the default
;; name
mu4e-view-buffer-name))
;; Search all view buffers and return those that are linked to
;; `headers-buffer'.
(linked-buffer (mu4e-get-view-buffers
(lambda (buf) (and (buffer-local-boundp 'mu4e-linked-headers-buffer buf)
(eq mu4e-linked-headers-buffer headers-buffer))))))
;; If such a linked buffer exists and its buffer is live, we use that buffer.
(if (and linked-buffer (buffer-live-p (car linked-buffer)))
;; NOTE: It's possible for there to be more than one linked view buffer.
;;
;; What, if anything, should the heuristic be to pick the
;; one to use? Presently `car' is used, but there are better
;; ways, no doubt. Perhaps preferring those with live windows?
(setq buffer (car linked-buffer))
(setq buffer (get-buffer buffer-name))
;; check if `buffer' is already live *and* detached. If it is,
;; we'll generate a new, unique name.
(when (and (buffer-live-p buffer) (mu4e--view-detached-p buffer))
(setq buffer (generate-new-buffer-name buffer-name)))
(when (or (not (buffer-live-p buffer)) create)
(setq buffer (get-buffer-create (or buffer buffer-name)))
(with-current-buffer buffer
(mu4e-view-mode))))
(when (and buffer (buffer-live-p buffer))
;; Required. Callers expect the view buffer to be set.
(set-buffer buffer)
;; Required. The call chain of `mu4e-view-mode' ends up
;; calling `kill-all-local-variables', which destroys the
;; local binding.
(set (make-local-variable 'mu4e-linked-headers-buffer) headers-buffer))
buffer)))
(defun mu4e-display-buffer (buffer-or-name &optional select)
"Display BUFFER-OR-NAME as per `mu4e-split-view'.
If SELECT is non-nil, the final window (and thus BUFFER-OR-NAME)
is selected.
This function internally uses `display-buffer' (or
`pop-to-buffer' if SELECT is non-nil).
It is therefore possible to change the display behavior by
modifying `display-buffer-alist'.
If `mu4e-split-view' is a function, then it must return a live window
for BUFFER-OR-NAME to be displayed in."
(if (functionp mu4e-split-view)
(set-window-buffer (funcall mu4e-split-view) buffer-or-name)
(let* ((buffer-name (or (get-buffer buffer-or-name)
(error "Buffer `%s' does not exist" buffer-or-name)))
(buffer-type (with-current-buffer buffer-name (mu4e--get-current-buffer-type)))
(direction (cons 'direction
(pcase (cons buffer-type mu4e-split-view)
;; views or headers can display
;; horz/vert depending on the value of
;; `mu4e-split-view'
(`(,(or 'view 'headers) . horizontal) 'below)
(`(,(or 'view 'headers) . vertical) 'right)
(`(,_ . t) nil))))
(window-size
(pcase (cons buffer-type mu4e-split-view)
;; views or headers can display
;; horz/vert depending on the value of
;; `mu4e-split-view'
('(view . horizontal) '((window-height . shrink-window-if-larger-than-buffer)))
('(view . vertical) '((window-min-width . fit-window-to-buffer)))
(`(,_ . t) nil)))
(window-action (cond
((and (eq buffer-type 'compose) mu4e-compose-in-new-frame)
'(display-buffer-pop-up-frame))
((memq buffer-type '(headers compose))
'(display-buffer-reuse-mode-window display-buffer-same-window))
((memq mu4e-split-view '(horizontal vertical))
'(display-buffer-in-direction))
((memq mu4e-split-view '(single-window))
'(display-buffer-same-window))
;; I cannot discern a difference between
;; `single-window' and "anything else" in
;; `mu4e-split-view'.
(t '(display-buffer-same-window))))
(arg `((display-buffer-reuse-window
display-buffer-reuse-mode-window
,@window-action)
,@window-size
,direction
)))
(funcall (if select #'pop-to-buffer #'display-buffer)
buffer-name
arg))))
(defun mu4e-resize-linked-headers-window ()
"Resizes the linked headers window belonging to a view.
Resizes the current headers view according to `mu4e-split-view'
and `mu4e-headers-visible-lines' or
`mu4e-headers-visible-columns'.
This function is best called from the hook
`mu4e-after-view-message-hook'."
(unless (mu4e-current-buffer-type-p 'view)
(error "Cannot resize as this is not a valid view buffer."))
(when-let (win (and mu4e-linked-headers-buffer
(get-buffer-window mu4e-linked-headers-buffer)))
;; This can fail for any number of reasons. If it does, we do
;; nothing. If the user has customized the window display we may
;; find it impossible to resize the window, and that should not be
;; cause for error.
(ignore-errors
(cond ((eq mu4e-split-view 'vertical)
(window-resize win (- mu4e-headers-visible-columns (window-width win nil))
t t nil))
((eq mu4e-split-view 'horizontal)
(set-window-text-height win mu4e-headers-visible-lines))))))
(provide 'mu4e-window)
;;; mu4e-window.el ends here

View File

@ -26,6 +26,7 @@
;;; Code:
(require 'mu4e-vars)
(require 'mu4e-window)
(require 'mu4e-obsolete)
(require 'mu4e-helpers)
(require 'mu4e-folders)

View File

@ -837,7 +837,7 @@ major-mode for the headers view is @code{mu4e-headers-mode}.
* Sorting and threading::Influencing the display
* Custom headers: HV Custom headers. Adding your own headers
* Actions: HV Actions. Defining and using actions
* Split view::Seeing both headers and messages
* Controlling and Displaying Buffers:: How and where the buffers are displayed
@end menu
@node HV Overview
@ -1117,14 +1117,24 @@ attachment, using @code{mu4e-compose-attach-captured-message}. See
@file{mu4e-actions.el} in the @t{mu4e} source distribution for more example
actions.
@node Split view
@section Split view
@node Controlling and Displaying Buffers
@section Display
Using the @emph{Split view}, we can see the @ref{Headers view} and the
@ref{Message view} next to each other, with the message selected in the
former, visible in the latter. You can influence the way the splitting
is done by customizing the variable @code{mu4e-split-view}. Possible
values are:
By default, @t{mu4e} will attempt to manage the display of its own
buffers. To do this, the variable @code{mu4e-split-view} is used to determine
how, or where, windows and buffers are placed.
However, @t{mu4e}'s display rules are provisional; you can override them
easily by customizing @code{display-buffer-alist}, which governs how Emacs --
and thus @t{mu4e} -- must display your buffers.
That means you can instruct @t{mu4e} to place message views in separate tabs
or frames, if you so desire.
@section Split view
You can control how @t{mu4e} displays its buffers, including the @ref{Headers
view} and the @ref{Message view}, by customizing
@code{mu4e-split-view}. There are several options available:
@itemize
@item @t{horizontal} (this is the default): display the message view below the
@ -1138,7 +1148,9 @@ minimize mu4e window operations (opening, killing, resizing, etc) and
buffer changes, while still retaining the view and headers buffers. In
addition, it replaces mu4e main view with a minibuffer prompt containing
the same information.
@item anything else: don't do any splitting
@item @t{function}: a function that takes a buffer name and returns a
window to display the buffer in.
@item anything else: prefer reusing the same window, if possible.
@end itemize
@noindent
@ -1151,6 +1163,26 @@ headers-view to the message-view and vice-versa with
@code{mu4e-select-other-view}, bound to @key{y}
@end itemize
@section Display Buffer Example
Here are a couple of examples that override @t{mu4e}'s default buffer
placement. You do not need to configure @code{mu4e-split-view} for this to
work. In the absence of explicit rules to the contrary, @t{mu4e} will fall
back on the value you have set in @code{mu4e-split-view} @emph{unless} you
have assigned your own custom window function.
Here is an example that displays the headers buffer in a side window to the
right. It occupies half of the width of the frame.
@lisp
(add-to-list 'display-buffer-alist
'(("\\*mu4e-headers\\*" display-buffer-in-side-window
(side . right)
(window-width . 0.5)))
@end lisp
You can type @key{C-x w s} to toggle the side windows to hide or show them at
will.
@node Message view
@chapter The message view
@ -1170,6 +1202,7 @@ from @t{gnus-article-mode}.
* Rich-text and images: MSGV Rich-text and images. Reading rich-text messages
* Custom headers: MSGV Custom headers. Your very own headers
* Actions: MSGV Actions. Defining and using actions.
* Detaching and Reattaching messages: MSGV Detaching and reattaching. Multiple message views.
@end menu
@node MSGV Overview
@ -1286,6 +1319,7 @@ A execute some custom action on the message's MIME-parts
misc
----
z, Z detach (or reattach) a message view to a headers buffer
. show the raw message view. 'q' takes you back.
C-+,C-- increase / decrease the number of headers shown
H get help
@ -1398,6 +1432,35 @@ as attachments. For now, these actions are defined and documented in
@code{mu4e-view-mime-part-actions}.
@node MSGV Detaching and reattaching
@section Detaching and reattaching messages
You can have multiple message views, but you must rename the view
buffer and detach it to stop @t{mu4e} from reusing it when you
navigate up or down in the headers buffer. If you have several view
buffers attached to a headers view, then @t{mu4e} may pick one at
random when it has to choose which one to display a message in.
To detach the message view from its linked headers buffer, type
@key{z}. A message will appear saying it is detached (or warn you if
it is already detached.)
Detached buffers are static; they cannot change the displayed message,
and no headers buffer will use a detached buffer to display its
messages. You can reattach a buffer to an live headers buffer by
typing @key{Z}.
You can freely rename a message view buffer -- such as with @key{C-x x
r} -- if you want multiple, open messages.
Detached messages are often useful for workflows involving lots of
simultaneous messages.
You can @emph{tear off} the window a message is in and place it in a
new tab by typing @key{C-x w ^ f}. You can also detach a window and
put it in its own tab with @key{C-x w ^ t}.
@node Editor view
@chapter The editor view
@ -4022,8 +4085,12 @@ Sending...done
The first and final messages are the most important, and there may be
considerable time between them, depending on the size of the message.
@subsection Is it possible to compose messages in a separate frame?
Yes --- set the variable @code{mu4e-compose-in-new-frame} to @code{t}.
@subsection Is it possible to view headers and messages, or compose new ones, in a separate frame?
Yes. There is builtin support for composing messages in a new
frame. Set the variable @code{mu4e-compose-in-new-frame} to @code{t}.
However, if you want to personalize the display of all of @t{mu4e}'s
buffers, you can do so by customizing @code{display-buffer-alist}.
@subsection How can I apply format=flowed to my outgoing messages?
This enables receiving clients that support this feature to reflow