Multiple buffer support and standardised window and buffer handling

This commit is contained in:
Mickey Petersen 2022-11-18 12:54:27 +00:00
parent 39a2c28777
commit 69a17bfcb9
13 changed files with 494 additions and 338 deletions

View File

@ -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)
@ -539,79 +539,82 @@ 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 switch-function))
(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))
;; TODO: replace this
(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 +758,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)))))

View File

@ -667,12 +667,7 @@ This is based on `mu4e-drafts-folder', which is evaluated once.")
(defun mu4e~draft-open-file (path switch-function)
"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)
@ -692,8 +687,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 +699,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) switch-function)))
(resend
;; case-2: copy some exisisting message to a draft message, then edit
@ -709,7 +707,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 switch-function))))
((reply forward new)
;; case-3: creating a new message; in this case, we can determine
@ -721,7 +719,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 switch-function))
(set-buffer draft-buffer)
(insert initial-contents)
(newline)
;; include the message signature (if it's set)
@ -739,7 +738,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

@ -724,9 +724,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 (buf) (eq docid (plist-get mu4e~view-message :docid)))))
;; note: this function is very performance-sensitive
(defun mu4e~headers-append-handler (msglst)
@ -808,10 +806,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))))
@ -825,7 +823,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)))
@ -842,7 +840,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))
@ -1155,7 +1153,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.
@ -1329,32 +1328,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
@ -1623,41 +1596,21 @@ _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)))
(mu4e--server-view docid mark-as-read)))
@ -1688,15 +1641,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
@ -1807,42 +1760,19 @@ 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)
(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

View File

@ -105,60 +105,25 @@ and `mu4e-headers-visible-columns'."
(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))
;;; TODO: rewrite select other window
(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)
@ -406,7 +371,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

@ -371,17 +371,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 +416,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

@ -99,30 +99,22 @@ 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)))
(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)))))
(mu4e-error "Cannot find message in headers buffer"))))))))
(defconst mu4e-marks
'((refile

View File

@ -206,18 +206,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-fontify-buffer)
;; 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'."
(let ((win (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

@ -355,7 +355,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 +363,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

@ -122,42 +122,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 +136,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 +151,31 @@ 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) (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 (mu4e-get-headers-buffer)
(mu4e-message-field (mu4e-message-at-point) :docid))))
,@body
(mu4e-error "Cannot find message in headers buffer"))))))
@ -226,12 +205,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 +311,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 +599,34 @@ 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)
;; create a new view buffer (if needed)
(setq gnus-article-buffer (mu4e-get-view-buffer nil t))
(with-current-buffer 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))
(mu4e-message-readable-path msg) nil nil nil t)
(setq-local mu4e~view-message msg)))
(mu4e~view-render-buffer msg)
(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))))
(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 +693,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 +832,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 +990,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 +1164,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 +1288,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

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

@ -0,0 +1,256 @@
;;; 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:
;; TODO: write function to get headers buffer (and return an error if it is missing)
;; TODO: write a function that displays the headers/view buffer(s)
;; TODO: write a function that returns buffer livenness for headers/view
;; TODO: write a function that returns whether a buffer is visible
;; TODO: write a function that selects the headers/view buffer if it exists or is visible.
;;; Buffer names for internal use
(defvar mu4e~headers-loading-buf nil
"A buffer for loading a message view.")
(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")
(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)
(or mu4e-linked-headers-buffer
(error "This view buffer is detached")))
;; 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 returns 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)
"Returns 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 ()
"Returns 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-loading-mode) 'loading)
((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)
"Returns non-nil if the current buffer is a mu4e buffer of TYPE.
Where TYPE is `view', `loading', `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 one 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 (and (not (buffer-live-p buffer)) create)
(setq buffer (get-buffer-create (or buffer buffer-name)))
(with-current-buffer buffer
(mu4e-view-mode))))
(when 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
((memq buffer-type '(loader)) '(display-buffer-same-window))
((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))))
(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)