~rjarry/aerc-devel

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
7 4

[PATCH aerc 1/2] ui: enable showing of entire-thread

Details
Message ID
<20230907164506.98688-3-tim@timculverhouse.com>
DKIM signature
missing
Download raw message
Patch: +57 -14
Add a UI config value to enable showing of an "entire thread", similar
to `notmuch show --entire-thread=true`. Add an associated style called
"msglist_borrowed" which can be used to style such messages.

Currently this feature is only supported by notmuch. It would be
possible for maildir to implement as well, IMAP with gmail custom
extensions, and JMAP. This patch merely implements the notmuch version
and puts the groundwork in for handling these sorts of displays.

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
---
 commands/commands_test.go      |  1 +
 config/aerc.conf               |  8 ++++++++
 config/style.go                |  2 ++
 config/templates.go            |  1 +
 config/ui.go                   |  1 +
 lib/msgstore.go                |  4 ++++
 lib/state/templates.go         | 10 ++++++++--
 models/templates.go            |  1 +
 widgets/account.go             |  1 +
 widgets/msglist.go             | 14 ++++++++++----
 worker/notmuch/lib/database.go | 19 ++++++++++++-------
 worker/notmuch/worker.go       |  4 +++-
 worker/types/messages.go       |  1 +
 worker/types/thread.go         |  4 ++++
 14 files changed, 57 insertions(+), 14 deletions(-)

diff --git a/commands/commands_test.go b/commands/commands_test.go
index a1518aaa6690..418e34374799 100644
--- a/commands/commands_test.go
+++ b/commands/commands_test.go
@@ -77,6 +77,7 @@ func (d *dummyData) Header(string) string                      { return "" }
func (d *dummyData) ThreadPrefix() string                      { return "└─>" }
func (d *dummyData) ThreadCount() int                          { return 0 }
func (d *dummyData) ThreadFolded() bool                        { return false }
func (d *dummyData) ThreadBorrowed() bool                      { return false }
func (d *dummyData) Subject() string                           { return "Re: [PATCH] hey" }
func (d *dummyData) SubjectBase() string                       { return "[PATCH] hey" }
func (d *dummyData) Number() int                               { return 0 }
diff --git a/config/aerc.conf b/config/aerc.conf
index 46b24f1c9d93..eb6925633e03 100644
--- a/config/aerc.conf
+++ b/config/aerc.conf
@@ -290,6 +290,14 @@
# Default: false
#force-client-threads=false

# Show entire thread enables messages which do not match the current query (or
# belong to the current mailbox) to be shown for context. These messages can be
# styled separately using "msglist_borrowed" in a styleset. This feature is not
# supported by all backends
#
# Default: false
#entire-thread=false

# Debounce client-side thread building
#
# Default: 50ms
diff --git a/config/style.go b/config/style.go
index 50c53de328b5..67f608bdc6f4 100644
--- a/config/style.go
+++ b/config/style.go
@@ -41,6 +41,7 @@ const (
	STYLE_MSGLIST_THREAD_FOLDED
	STYLE_MSGLIST_GUTTER
	STYLE_MSGLIST_PILL
	STYLE_MSGLIST_BORROWED

	STYLE_DIRLIST_DEFAULT
	STYLE_DIRLIST_UNREAD
@@ -86,6 +87,7 @@ var StyleNames = map[string]StyleObject{
	"msglist_pill":     STYLE_MSGLIST_PILL,

	"msglist_thread_folded": STYLE_MSGLIST_THREAD_FOLDED,
	"msglist_borrowed":      STYLE_MSGLIST_BORROWED,

	"dirlist_default": STYLE_DIRLIST_DEFAULT,
	"dirlist_unread":  STYLE_DIRLIST_UNREAD,
diff --git a/config/templates.go b/config/templates.go
index 3f0249dfccef..e81e42ac6c6c 100644
--- a/config/templates.go
+++ b/config/templates.go
@@ -78,6 +78,7 @@ func (d *dummyData) Header(string) string            { return "" }
func (d *dummyData) ThreadPrefix() string            { return "└─>" }
func (d *dummyData) ThreadCount() int                { return 0 }
func (d *dummyData) ThreadFolded() bool              { return false }
func (d *dummyData) ThreadBorrowed() bool            { return true }
func (d *dummyData) Subject() string                 { return "Re: [PATCH] hey" }
func (d *dummyData) SubjectBase() string             { return "[PATCH] hey" }
func (d *dummyData) Attach(string) string            { return "" }
diff --git a/config/ui.go b/config/ui.go
index 699e8a0f66c4..7c0d52494cfd 100644
--- a/config/ui.go
+++ b/config/ui.go
@@ -41,6 +41,7 @@ type UIConfig struct {
	ThreadingEnabled              bool          `ini:"threading-enabled"`
	ForceClientThreads            bool          `ini:"force-client-threads"`
	ClientThreadsDelay            time.Duration `ini:"client-threads-delay" default:"50ms"`
	EntireThread                  bool          `ini:"entire-thread"`
	FuzzyComplete                 bool          `ini:"fuzzy-complete"`
	NewMessageBell                bool          `ini:"new-message-bell" default:"true"`
	Spinner                       string        `ini:"spinner" default:"[..]    , [..]   ,  [..]  ,   [..] ,    [..],   [..] ,  [..]  , [..]   "`
diff --git a/lib/msgstore.go b/lib/msgstore.go
index 01b782898318..9cf99371001e 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -49,6 +49,7 @@ type MessageStore struct {

	threadedView       bool
	reverseThreadOrder bool
	entireThread       bool
	sortThreadSiblings bool
	buildThreads       bool
	builder            *ThreadBuilder
@@ -87,6 +88,7 @@ func NewMessageStore(worker *types.Worker,
	reverseOrder bool, reverseThreadOrder bool, sortThreadSiblings bool,
	triggerNewEmail func(*models.MessageInfo),
	triggerDirectoryChange func(), onSelect func(*models.MessageInfo),
	entireThread bool,
) *MessageStore {
	if !worker.Backend.Capabilities().Thread {
		clientThreads = true
@@ -104,6 +106,7 @@ func NewMessageStore(worker *types.Worker,

		threadedView:       thread,
		buildThreads:       clientThreads,
		entireThread:       entireThread,
		reverseThreadOrder: reverseThreadOrder,
		sortThreadSiblings: sortThreadSiblings,

@@ -831,6 +834,7 @@ func (store *MessageStore) Sort(criteria []*types.SortCriterion, cb func(types.W
			Context:        store.ctx,
			SortCriteria:   criteria,
			FilterCriteria: store.filter,
			EntireThread:   store.entireThread,
		}, handle_return)
	} else {
		store.worker.PostAction(&types.FetchDirectoryContents{
diff --git a/lib/state/templates.go b/lib/state/templates.go
index 22d065894d50..b10c1133c622 100644
--- a/lib/state/templates.go
+++ b/lib/state/templates.go
@@ -20,7 +20,7 @@ type DataSetter interface {
	Data() models.TemplateData
	SetHeaders(*mail.Header, *models.OriginalMail)
	SetInfo(*models.MessageInfo, int, bool)
	SetThreading(string, bool, int, bool)
	SetThreading(string, bool, int, bool, bool)
	SetComposer(Composer)
	SetAccount(*config.AccountConfig)
	SetFolder(*models.Directory)
@@ -34,6 +34,7 @@ type ThreadInfo struct {
	Prefix      string
	Count       int
	Folded      bool
	Borrowed    bool
}

type templateData struct {
@@ -86,12 +87,13 @@ func (d *templateData) SetInfo(info *models.MessageInfo, num int, marked bool,
}

func (d *templateData) SetThreading(prefix string, same bool, count int,
	folded bool,
	folded bool, borrowed bool,
) {
	d.threadInfo.Prefix = prefix
	d.threadInfo.SameSubject = same
	d.threadInfo.Count = count
	d.threadInfo.Folded = folded
	d.threadInfo.Borrowed = borrowed
}

func (d *templateData) SetAccount(acct *config.AccountConfig) {
@@ -300,6 +302,10 @@ func (d *templateData) ThreadFolded() bool {
	return d.threadInfo.Folded
}

func (d *templateData) ThreadBorrowed() bool {
	return d.threadInfo.Borrowed
}

func (d *templateData) Subject() string {
	var subject string
	switch {
diff --git a/models/templates.go b/models/templates.go
index 0c684e863905..92cef2acb458 100644
--- a/models/templates.go
+++ b/models/templates.go
@@ -22,6 +22,7 @@ type TemplateData interface {
	ThreadPrefix() string
	ThreadCount() int
	ThreadFolded() bool
	ThreadBorrowed() bool
	Subject() string
	SubjectBase() string
	Number() int
diff --git a/widgets/account.go b/widgets/account.go
index 47bc992427bc..b5c71c297e73 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -255,6 +255,7 @@ func (acct *AccountView) newStore(name string) *lib.MessageStore {
			}
		},
		acct.updateSplitView,
		acct.dirlist.UiConfig(name).EntireThread,
	)
	store.SetMarker(marker.New(store))
	return store
diff --git a/widgets/msglist.go b/widgets/msglist.go
index dcb4cd31fb56..9371e4953d8e 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -209,8 +209,13 @@ func addMessage(
	}
	// folded thread
	templateData, ok := data.(models.TemplateData)
	if ok && templateData.ThreadFolded() {
		params.styles = append(params.styles, config.STYLE_MSGLIST_THREAD_FOLDED)
	if ok {
		if templateData.ThreadFolded() {
			params.styles = append(params.styles, config.STYLE_MSGLIST_THREAD_FOLDED)
		}
		if templateData.ThreadBorrowed() {
			params.styles = append(params.styles, config.STYLE_MSGLIST_BORROWED)
		}
	}
	// marked message
	marked := store.Marker().IsMarked(msg.Uid)
@@ -476,7 +481,7 @@ func newThreadView(store *lib.MessageStore) *threadView {
}

func (t *threadView) Update(data state.DataSetter, uid uint32) {
	prefix, same, count, folded := "", false, 0, false
	prefix, same, count, folded, brwd := "", false, 0, false, false
	thread, err := t.store.Thread(uid)
	if thread != nil && err == nil {
		prefix = threadPrefix(thread, t.reverse, true)
@@ -486,6 +491,7 @@ func (t *threadView) Update(data state.DataSetter, uid uint32) {
		t.prevSubj = subject
		count = countThreads(thread)
		folded = thread.FirstChild != nil && thread.FirstChild.Hidden
		brwd = thread.Borrowed
	}
	data.SetThreading(prefix, same, count, folded)
	data.SetThreading(prefix, same, count, folded, brwd)
}
diff --git a/worker/notmuch/lib/database.go b/worker/notmuch/lib/database.go
index 9a6689c4b57b..238de2147808 100644
--- a/worker/notmuch/lib/database.go
+++ b/worker/notmuch/lib/database.go
@@ -111,7 +111,7 @@ func (db *DB) MsgIDsFromQuery(ctx context.Context, q string) ([]string, error) {
	return msgIDs, err
}

func (db *DB) ThreadsFromQuery(ctx context.Context, q string) ([]*types.Thread, error) {
func (db *DB) ThreadsFromQuery(ctx context.Context, q string, entireThread bool) ([]*types.Thread, error) {
	query, err := db.newQuery(q)
	if err != nil {
		return nil, err
@@ -136,7 +136,7 @@ func (db *DB) ThreadsFromQuery(ctx context.Context, q string) ([]*types.Thread,
		default:
			thread := threads.Thread()
			tlm := thread.TopLevelMessages()
			root := db.makeThread(nil, &tlm)
			root := db.makeThread(nil, &tlm, entireThread)
			res = append(res, root)
			tlm.Close()
			thread.Close()
@@ -306,7 +306,7 @@ func (db *DB) KeyFromUid(uid uint32) (string, bool) {
	return db.uidStore.GetKey(uid)
}

func (db *DB) makeThread(parent *types.Thread, msgs *notmuch.Messages) *types.Thread {
func (db *DB) makeThread(parent *types.Thread, msgs *notmuch.Messages, entireThread bool) *types.Thread {
	var lastSibling *types.Thread
	for msgs.Next() {
		msg := msgs.Message()
@@ -319,14 +319,19 @@ func (db *DB) makeThread(parent *types.Thread, msgs *notmuch.Messages) *types.Th
		}
		replies := msg.Replies()
		defer replies.Close()
		if !match {
			parent = db.makeThread(parent, &replies)
		if !match && !entireThread {
			parent = db.makeThread(parent, &replies, entireThread)
			continue
		}
		node := &types.Thread{
			Uid:    db.uidStore.GetOrInsert(msgID),
			Parent: parent,
			Hidden: !match,
		}
		switch entireThread {
		case true:
			node.Borrowed = !match
		default:
			node.Hidden = !match
		}
		if parent != nil && parent.FirstChild == nil {
			parent.FirstChild = node
@@ -340,7 +345,7 @@ func (db *DB) makeThread(parent *types.Thread, msgs *notmuch.Messages) *types.Th
			lastSibling.NextSibling = node
		}
		lastSibling = node
		db.makeThread(node, &replies)
		db.makeThread(node, &replies, entireThread)
	}

	// We want to return the root node
diff --git a/worker/notmuch/worker.go b/worker/notmuch/worker.go
index d1eb69d011aa..809085c8ac74 100644
--- a/worker/notmuch/worker.go
+++ b/worker/notmuch/worker.go
@@ -635,6 +635,7 @@ func (w *worker) emitDirectoryContents(parent types.WorkerMessage) error {
func (w *worker) emitDirectoryThreaded(parent types.WorkerMessage) error {
	query := w.query
	ctx := context.Background()
	entireThread := false
	if msg, ok := parent.(*types.FetchDirectoryThreaded); ok {
		log.Debugf("filter input: '%v'", msg.FilterCriteria)
		s, err := translate(msg.FilterCriteria)
@@ -646,8 +647,9 @@ func (w *worker) emitDirectoryThreaded(parent types.WorkerMessage) error {
			log.Debugf("filter query: '%s'", query)
		}
		ctx = msg.Context
		entireThread = msg.EntireThread
	}
	threads, err := w.db.ThreadsFromQuery(ctx, query)
	threads, err := w.db.ThreadsFromQuery(ctx, query, entireThread)
	if err != nil {
		return err
	}
diff --git a/worker/types/messages.go b/worker/types/messages.go
index 26408684a4ba..ec3b1ed87335 100644
--- a/worker/types/messages.go
+++ b/worker/types/messages.go
@@ -115,6 +115,7 @@ type FetchDirectoryThreaded struct {
	Context        context.Context
	SortCriteria   []*SortCriterion
	FilterCriteria []string
	EntireThread   bool
}

type SearchDirectory struct {
diff --git a/worker/types/thread.go b/worker/types/thread.go
index 2f739bc28ed3..f67c7f36c2eb 100644
--- a/worker/types/thread.go
+++ b/worker/types/thread.go
@@ -17,6 +17,10 @@ type Thread struct {

	Hidden  bool // if this flag is set the message isn't rendered in the UI
	Deleted bool // if this flag is set the message was deleted

	// Borrowed indicates the message doesn't match the mailbox / query but
	// is displayed for context
	Borrowed bool
}

// AddChild appends the child node at the end of the existing children of t.
-- 
2.42.0

[PATCH aerc 2/2] commands: add :toggle-entire-thread command

Details
Message ID
<20230907164506.98688-4-tim@timculverhouse.com>
In-Reply-To
<20230907164506.98688-3-tim@timculverhouse.com> (view parent)
DKIM signature
missing
Download raw message
Patch: +49 -0
Add a command to toggle the display of an entire-thread. Update
CHANGELOG

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
---
 CHANGELOG.md                         |  1 +
 commands/msg/toggle-entire-thread.go | 36 ++++++++++++++++++++++++++++
 doc/aerc.1.scd                       |  4 ++++
 lib/msgstore.go                      |  8 +++++++
 4 files changed, 49 insertions(+)
 create mode 100644 commands/msg/toggle-entire-thread.go

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20a1697fbe66..5d2cbba7eeef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
  temporary file from which selected files will be read instead of the standard
  output.
- Save drafts in custom folders with `:postpone -t <folder>`.
- View "entire-thread" in notmuch backends with `:toggle-entire-thread`

### Fixed

diff --git a/commands/msg/toggle-entire-thread.go b/commands/msg/toggle-entire-thread.go
new file mode 100644
index 000000000000..8b23d6d541aa
--- /dev/null
+++ b/commands/msg/toggle-entire-thread.go
@@ -0,0 +1,36 @@
package msg

import (
	"errors"

	"git.sr.ht/~rjarry/aerc/lib/ui"
	"git.sr.ht/~rjarry/aerc/widgets"
)

type ToggleEntireThread struct{}

func init() {
	register(ToggleEntireThread{})
}

func (ToggleEntireThread) Aliases() []string {
	return []string{"toggle-entire-thread"}
}

func (ToggleEntireThread) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func (ToggleEntireThread) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: toggle-entire-thread")
	}
	h := newHelper(aerc)
	store, err := h.store()
	if err != nil {
		return err
	}
	store.ToggleEntireThread()
	ui.Invalidate()
	return nil
}
diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index 440cfc291679..e56443ea3c0b 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -469,6 +469,10 @@ message list, the message in the message viewer, etc).
*:toggle-threads*
	Toggles between message threading and the normal message list.

*:toggle-entire-thread*
	Toggles between showing entire thread (when supported) and only showing
	messages which match the current query / mailbox.

*:view* [*-p*]++
*:view-message* [*-p*]
	Opens the message viewer to display the selected message. If the peek
diff --git a/lib/msgstore.go b/lib/msgstore.go
index 9cf99371001e..79148868a80b 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -442,6 +442,14 @@ func (store *MessageStore) ThreadedView() bool {
	return store.threadedView
}

func (store *MessageStore) ToggleEntireThread() {
	if !store.threadedView {
		return
	}
	store.entireThread = !store.entireThread
	store.Sort(store.sortCriteria, nil)
}

func (store *MessageStore) BuildThreads() bool {
	return store.buildThreads
}
-- 
2.42.0

[aerc/patches] build success

builds.sr.ht <builds@sr.ht>
Details
Message ID
<CVCUCWVSSYBL.3N7AXXFNOE32W@cirno2>
In-Reply-To
<20230907164506.98688-4-tim@timculverhouse.com> (view parent)
DKIM signature
missing
Download raw message
aerc/patches: SUCCESS in 5m13s

[ui: enable showing of entire-thread][0] from [Tim Culverhouse][1]

[0]: https://lists.sr.ht/~rjarry/aerc-devel/patches/44468
[1]: tim@timculverhouse.com

✓ #1054203 SUCCESS aerc/patches/openbsd.yml     https://builds.sr.ht/~rjarry/job/1054203
✓ #1054202 SUCCESS aerc/patches/alpine-edge.yml https://builds.sr.ht/~rjarry/job/1054202
Details
Message ID
<CVDDA6NN8YW1.3M4GK0TW6NVEI@sindominio.net>
In-Reply-To
<20230907164506.98688-3-tim@timculverhouse.com> (view parent)
DKIM signature
missing
Download raw message
This is great. Setting mgslist_borrowed.dim=true works nicely. Thanks!

Tested-By: inwit <inwit@sindominio.net>	


On 07/09/2023, 18:45, Tim Culverhouse wrote:
> Add a UI config value to enable showing of an "entire thread", similar
> to `notmuch show --entire-thread=true`. Add an associated style called
> "msglist_borrowed" which can be used to style such messages.
>
> Currently this feature is only supported by notmuch. It would be
> possible for maildir to implement as well, IMAP with gmail custom
> extensions, and JMAP. This patch merely implements the notmuch version
> and puts the groundwork in for handling these sorts of displays.
>
> Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
> ---
>  commands/commands_test.go      |  1 +
>  config/aerc.conf               |  8 ++++++++
>  config/style.go                |  2 ++
>  config/templates.go            |  1 +
>  config/ui.go                   |  1 +
>  lib/msgstore.go                |  4 ++++
>  lib/state/templates.go         | 10 ++++++++--
>  models/templates.go            |  1 +
>  widgets/account.go             |  1 +
>  widgets/msglist.go             | 14 ++++++++++----
>  worker/notmuch/lib/database.go | 19 ++++++++++++-------
>  worker/notmuch/worker.go       |  4 +++-
>  worker/types/messages.go       |  1 +
>  worker/types/thread.go         |  4 ++++
>  14 files changed, 57 insertions(+), 14 deletions(-)
>
> diff --git a/commands/commands_test.go b/commands/commands_test.go
> index a1518aaa6690..418e34374799 100644
> --- a/commands/commands_test.go
> +++ b/commands/commands_test.go
> @@ -77,6 +77,7 @@ func (d *dummyData) Header(string) string                      { return "" }
>  func (d *dummyData) ThreadPrefix() string                      { return "└─>" }
>  func (d *dummyData) ThreadCount() int                          { return 0 }
>  func (d *dummyData) ThreadFolded() bool                        { return false }
> +func (d *dummyData) ThreadBorrowed() bool                      { return false }
>  func (d *dummyData) Subject() string                           { return "Re: [PATCH] hey" }
>  func (d *dummyData) SubjectBase() string                       { return "[PATCH] hey" }
>  func (d *dummyData) Number() int                               { return 0 }
> diff --git a/config/aerc.conf b/config/aerc.conf
> index 46b24f1c9d93..eb6925633e03 100644
> --- a/config/aerc.conf
> +++ b/config/aerc.conf
> @@ -290,6 +290,14 @@
>  # Default: false
>  #force-client-threads=false
>  
> +# Show entire thread enables messages which do not match the current query (or
> +# belong to the current mailbox) to be shown for context. These messages can be
> +# styled separately using "msglist_borrowed" in a styleset. This feature is not
> +# supported by all backends
> +#
> +# Default: false
> +#entire-thread=false
> +
>  # Debounce client-side thread building
>  #
>  # Default: 50ms
> diff --git a/config/style.go b/config/style.go
> index 50c53de328b5..67f608bdc6f4 100644
> --- a/config/style.go
> +++ b/config/style.go
> @@ -41,6 +41,7 @@ const (
>  	STYLE_MSGLIST_THREAD_FOLDED
>  	STYLE_MSGLIST_GUTTER
>  	STYLE_MSGLIST_PILL
> +	STYLE_MSGLIST_BORROWED
>  
>  	STYLE_DIRLIST_DEFAULT
>  	STYLE_DIRLIST_UNREAD
> @@ -86,6 +87,7 @@ var StyleNames = map[string]StyleObject{
>  	"msglist_pill":     STYLE_MSGLIST_PILL,
>  
>  	"msglist_thread_folded": STYLE_MSGLIST_THREAD_FOLDED,
> +	"msglist_borrowed":      STYLE_MSGLIST_BORROWED,
>  
>  	"dirlist_default": STYLE_DIRLIST_DEFAULT,
>  	"dirlist_unread":  STYLE_DIRLIST_UNREAD,
> diff --git a/config/templates.go b/config/templates.go
> index 3f0249dfccef..e81e42ac6c6c 100644
> --- a/config/templates.go
> +++ b/config/templates.go
> @@ -78,6 +78,7 @@ func (d *dummyData) Header(string) string            { return "" }
>  func (d *dummyData) ThreadPrefix() string            { return "└─>" }
>  func (d *dummyData) ThreadCount() int                { return 0 }
>  func (d *dummyData) ThreadFolded() bool              { return false }
> +func (d *dummyData) ThreadBorrowed() bool            { return true }
>  func (d *dummyData) Subject() string                 { return "Re: [PATCH] hey" }
>  func (d *dummyData) SubjectBase() string             { return "[PATCH] hey" }
>  func (d *dummyData) Attach(string) string            { return "" }
> diff --git a/config/ui.go b/config/ui.go
> index 699e8a0f66c4..7c0d52494cfd 100644
> --- a/config/ui.go
> +++ b/config/ui.go
> @@ -41,6 +41,7 @@ type UIConfig struct {
>  	ThreadingEnabled              bool          `ini:"threading-enabled"`
>  	ForceClientThreads            bool          `ini:"force-client-threads"`
>  	ClientThreadsDelay            time.Duration `ini:"client-threads-delay" default:"50ms"`
> +	EntireThread                  bool          `ini:"entire-thread"`
>  	FuzzyComplete                 bool          `ini:"fuzzy-complete"`
>  	NewMessageBell                bool          `ini:"new-message-bell" default:"true"`
>  	Spinner                       string        `ini:"spinner" default:"[..]    , [..]   ,  [..]  ,   [..] ,    [..],   [..] ,  [..]  , [..]   "`
> diff --git a/lib/msgstore.go b/lib/msgstore.go
> index 01b782898318..9cf99371001e 100644
> --- a/lib/msgstore.go
> +++ b/lib/msgstore.go
> @@ -49,6 +49,7 @@ type MessageStore struct {
>  
>  	threadedView       bool
>  	reverseThreadOrder bool
> +	entireThread       bool
>  	sortThreadSiblings bool
>  	buildThreads       bool
>  	builder            *ThreadBuilder
> @@ -87,6 +88,7 @@ func NewMessageStore(worker *types.Worker,
>  	reverseOrder bool, reverseThreadOrder bool, sortThreadSiblings bool,
>  	triggerNewEmail func(*models.MessageInfo),
>  	triggerDirectoryChange func(), onSelect func(*models.MessageInfo),
> +	entireThread bool,
>  ) *MessageStore {
>  	if !worker.Backend.Capabilities().Thread {
>  		clientThreads = true
> @@ -104,6 +106,7 @@ func NewMessageStore(worker *types.Worker,
>  
>  		threadedView:       thread,
>  		buildThreads:       clientThreads,
> +		entireThread:       entireThread,
>  		reverseThreadOrder: reverseThreadOrder,
>  		sortThreadSiblings: sortThreadSiblings,
>  
> @@ -831,6 +834,7 @@ func (store *MessageStore) Sort(criteria []*types.SortCriterion, cb func(types.W
>  			Context:        store.ctx,
>  			SortCriteria:   criteria,
>  			FilterCriteria: store.filter,
> +			EntireThread:   store.entireThread,
>  		}, handle_return)
>  	} else {
>  		store.worker.PostAction(&types.FetchDirectoryContents{
> diff --git a/lib/state/templates.go b/lib/state/templates.go
> index 22d065894d50..b10c1133c622 100644
> --- a/lib/state/templates.go
> +++ b/lib/state/templates.go
> @@ -20,7 +20,7 @@ type DataSetter interface {
>  	Data() models.TemplateData
>  	SetHeaders(*mail.Header, *models.OriginalMail)
>  	SetInfo(*models.MessageInfo, int, bool)
> -	SetThreading(string, bool, int, bool)
> +	SetThreading(string, bool, int, bool, bool)
>  	SetComposer(Composer)
>  	SetAccount(*config.AccountConfig)
>  	SetFolder(*models.Directory)
> @@ -34,6 +34,7 @@ type ThreadInfo struct {
>  	Prefix      string
>  	Count       int
>  	Folded      bool
> +	Borrowed    bool
>  }
>  
>  type templateData struct {
> @@ -86,12 +87,13 @@ func (d *templateData) SetInfo(info *models.MessageInfo, num int, marked bool,
>  }
>  
>  func (d *templateData) SetThreading(prefix string, same bool, count int,
> -	folded bool,
> +	folded bool, borrowed bool,
>  ) {
>  	d.threadInfo.Prefix = prefix
>  	d.threadInfo.SameSubject = same
>  	d.threadInfo.Count = count
>  	d.threadInfo.Folded = folded
> +	d.threadInfo.Borrowed = borrowed
>  }
>  
>  func (d *templateData) SetAccount(acct *config.AccountConfig) {
> @@ -300,6 +302,10 @@ func (d *templateData) ThreadFolded() bool {
>  	return d.threadInfo.Folded
>  }
>  
> +func (d *templateData) ThreadBorrowed() bool {
> +	return d.threadInfo.Borrowed
> +}
> +
>  func (d *templateData) Subject() string {
>  	var subject string
>  	switch {
> diff --git a/models/templates.go b/models/templates.go
> index 0c684e863905..92cef2acb458 100644
> --- a/models/templates.go
> +++ b/models/templates.go
> @@ -22,6 +22,7 @@ type TemplateData interface {
>  	ThreadPrefix() string
>  	ThreadCount() int
>  	ThreadFolded() bool
> +	ThreadBorrowed() bool
>  	Subject() string
>  	SubjectBase() string
>  	Number() int
> diff --git a/widgets/account.go b/widgets/account.go
> index 47bc992427bc..b5c71c297e73 100644
> --- a/widgets/account.go
> +++ b/widgets/account.go
> @@ -255,6 +255,7 @@ func (acct *AccountView) newStore(name string) *lib.MessageStore {
>  			}
>  		},
>  		acct.updateSplitView,
> +		acct.dirlist.UiConfig(name).EntireThread,
>  	)
>  	store.SetMarker(marker.New(store))
>  	return store
> diff --git a/widgets/msglist.go b/widgets/msglist.go
> index dcb4cd31fb56..9371e4953d8e 100644
> --- a/widgets/msglist.go
> +++ b/widgets/msglist.go
> @@ -209,8 +209,13 @@ func addMessage(
>  	}
>  	// folded thread
>  	templateData, ok := data.(models.TemplateData)
> -	if ok && templateData.ThreadFolded() {
> -		params.styles = append(params.styles, config.STYLE_MSGLIST_THREAD_FOLDED)
> +	if ok {
> +		if templateData.ThreadFolded() {
> +			params.styles = append(params.styles, config.STYLE_MSGLIST_THREAD_FOLDED)
> +		}
> +		if templateData.ThreadBorrowed() {
> +			params.styles = append(params.styles, config.STYLE_MSGLIST_BORROWED)
> +		}
>  	}
>  	// marked message
>  	marked := store.Marker().IsMarked(msg.Uid)
> @@ -476,7 +481,7 @@ func newThreadView(store *lib.MessageStore) *threadView {
>  }
>  
>  func (t *threadView) Update(data state.DataSetter, uid uint32) {
> -	prefix, same, count, folded := "", false, 0, false
> +	prefix, same, count, folded, brwd := "", false, 0, false, false
>  	thread, err := t.store.Thread(uid)
>  	if thread != nil && err == nil {
>  		prefix = threadPrefix(thread, t.reverse, true)
> @@ -486,6 +491,7 @@ func (t *threadView) Update(data state.DataSetter, uid uint32) {
>  		t.prevSubj = subject
>  		count = countThreads(thread)
>  		folded = thread.FirstChild != nil && thread.FirstChild.Hidden
> +		brwd = thread.Borrowed
>  	}
> -	data.SetThreading(prefix, same, count, folded)
> +	data.SetThreading(prefix, same, count, folded, brwd)
>  }
> diff --git a/worker/notmuch/lib/database.go b/worker/notmuch/lib/database.go
> index 9a6689c4b57b..238de2147808 100644
> --- a/worker/notmuch/lib/database.go
> +++ b/worker/notmuch/lib/database.go
> @@ -111,7 +111,7 @@ func (db *DB) MsgIDsFromQuery(ctx context.Context, q string) ([]string, error) {
>  	return msgIDs, err
>  }
>  
> -func (db *DB) ThreadsFromQuery(ctx context.Context, q string) ([]*types.Thread, error) {
> +func (db *DB) ThreadsFromQuery(ctx context.Context, q string, entireThread bool) ([]*types.Thread, error) {
>  	query, err := db.newQuery(q)
>  	if err != nil {
>  		return nil, err
> @@ -136,7 +136,7 @@ func (db *DB) ThreadsFromQuery(ctx context.Context, q string) ([]*types.Thread,
>  		default:
>  			thread := threads.Thread()
>  			tlm := thread.TopLevelMessages()
> -			root := db.makeThread(nil, &tlm)
> +			root := db.makeThread(nil, &tlm, entireThread)
>  			res = append(res, root)
>  			tlm.Close()
>  			thread.Close()
> @@ -306,7 +306,7 @@ func (db *DB) KeyFromUid(uid uint32) (string, bool) {
>  	return db.uidStore.GetKey(uid)
>  }
>  
> -func (db *DB) makeThread(parent *types.Thread, msgs *notmuch.Messages) *types.Thread {
> +func (db *DB) makeThread(parent *types.Thread, msgs *notmuch.Messages, entireThread bool) *types.Thread {
>  	var lastSibling *types.Thread
>  	for msgs.Next() {
>  		msg := msgs.Message()
> @@ -319,14 +319,19 @@ func (db *DB) makeThread(parent *types.Thread, msgs *notmuch.Messages) *types.Th
>  		}
>  		replies := msg.Replies()
>  		defer replies.Close()
> -		if !match {
> -			parent = db.makeThread(parent, &replies)
> +		if !match && !entireThread {
> +			parent = db.makeThread(parent, &replies, entireThread)
>  			continue
>  		}
>  		node := &types.Thread{
>  			Uid:    db.uidStore.GetOrInsert(msgID),
>  			Parent: parent,
> -			Hidden: !match,
> +		}
> +		switch entireThread {
> +		case true:
> +			node.Borrowed = !match
> +		default:
> +			node.Hidden = !match
>  		}
>  		if parent != nil && parent.FirstChild == nil {
>  			parent.FirstChild = node
> @@ -340,7 +345,7 @@ func (db *DB) makeThread(parent *types.Thread, msgs *notmuch.Messages) *types.Th
>  			lastSibling.NextSibling = node
>  		}
>  		lastSibling = node
> -		db.makeThread(node, &replies)
> +		db.makeThread(node, &replies, entireThread)
>  	}
>  
>  	// We want to return the root node
> diff --git a/worker/notmuch/worker.go b/worker/notmuch/worker.go
> index d1eb69d011aa..809085c8ac74 100644
> --- a/worker/notmuch/worker.go
> +++ b/worker/notmuch/worker.go
> @@ -635,6 +635,7 @@ func (w *worker) emitDirectoryContents(parent types.WorkerMessage) error {
>  func (w *worker) emitDirectoryThreaded(parent types.WorkerMessage) error {
>  	query := w.query
>  	ctx := context.Background()
> +	entireThread := false
>  	if msg, ok := parent.(*types.FetchDirectoryThreaded); ok {
>  		log.Debugf("filter input: '%v'", msg.FilterCriteria)
>  		s, err := translate(msg.FilterCriteria)
> @@ -646,8 +647,9 @@ func (w *worker) emitDirectoryThreaded(parent types.WorkerMessage) error {
>  			log.Debugf("filter query: '%s'", query)
>  		}
>  		ctx = msg.Context
> +		entireThread = msg.EntireThread
>  	}
> -	threads, err := w.db.ThreadsFromQuery(ctx, query)
> +	threads, err := w.db.ThreadsFromQuery(ctx, query, entireThread)
>  	if err != nil {
>  		return err
>  	}
> diff --git a/worker/types/messages.go b/worker/types/messages.go
> index 26408684a4ba..ec3b1ed87335 100644
> --- a/worker/types/messages.go
> +++ b/worker/types/messages.go
> @@ -115,6 +115,7 @@ type FetchDirectoryThreaded struct {
>  	Context        context.Context
>  	SortCriteria   []*SortCriterion
>  	FilterCriteria []string
> +	EntireThread   bool
>  }
>  
>  type SearchDirectory struct {
> diff --git a/worker/types/thread.go b/worker/types/thread.go
> index 2f739bc28ed3..f67c7f36c2eb 100644
> --- a/worker/types/thread.go
> +++ b/worker/types/thread.go
> @@ -17,6 +17,10 @@ type Thread struct {
>  
>  	Hidden  bool // if this flag is set the message isn't rendered in the UI
>  	Deleted bool // if this flag is set the message was deleted
> +
> +	// Borrowed indicates the message doesn't match the mailbox / query but
> +	// is displayed for context
> +	Borrowed bool
>  }
>  
>  // AddChild appends the child node at the end of the existing children of t.
> -- 
> 2.42.0
Details
Message ID
<CVN7HI3TH4QI.204OQUG27Q75K@ringo>
In-Reply-To
<20230907164506.98688-3-tim@timculverhouse.com> (view parent)
DKIM signature
missing
Download raw message
Tim Culverhouse, Sep 07, 2023 at 18:50:
> Add a UI config value to enable showing of an "entire thread", similar
> to `notmuch show --entire-thread=true`. Add an associated style called
> "msglist_borrowed" which can be used to style such messages.
>
> Currently this feature is only supported by notmuch. It would be
> possible for maildir to implement as well, IMAP with gmail custom
> extensions, and JMAP. This patch merely implements the notmuch version
> and puts the groundwork in for handling these sorts of displays.
>
> Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>

Hey Tim,

This is nice, I hope we'll get this for other backends soon(tm) :)

> diff --git a/config/aerc.conf b/config/aerc.conf
> index 46b24f1c9d93..eb6925633e03 100644
> --- a/config/aerc.conf
> +++ b/config/aerc.conf
> @@ -290,6 +290,14 @@
>  # Default: false
>  #force-client-threads=false
>  
> +# Show entire thread enables messages which do not match the current query (or
> +# belong to the current mailbox) to be shown for context. These messages can be
> +# styled separately using "msglist_borrowed" in a styleset. This feature is not
> +# supported by all backends
> +#
> +# Default: false
> +#entire-thread=false
> +

How about "show-thread-context" instead to match the style object name
(see below)?

Also, you forgot to add this in aerc-config(5).

>  # Debounce client-side thread building
>  #
>  # Default: 50ms
> diff --git a/config/style.go b/config/style.go
> index 50c53de328b5..67f608bdc6f4 100644
> --- a/config/style.go
> +++ b/config/style.go
> @@ -41,6 +41,7 @@ const (
>  	STYLE_MSGLIST_THREAD_FOLDED
>  	STYLE_MSGLIST_GUTTER
>  	STYLE_MSGLIST_PILL
> +	STYLE_MSGLIST_BORROWED
>  
>  	STYLE_DIRLIST_DEFAULT
>  	STYLE_DIRLIST_UNREAD
> @@ -86,6 +87,7 @@ var StyleNames = map[string]StyleObject{
>  	"msglist_pill":     STYLE_MSGLIST_PILL,
>  
>  	"msglist_thread_folded": STYLE_MSGLIST_THREAD_FOLDED,
> +	"msglist_borrowed":      STYLE_MSGLIST_BORROWED,

The "borrowed" term feels weird. How about msglist_thread_context
instead?
Details
Message ID
<CVNJVW205TG4.3KGG8GMOH1P7H@sindominio.net>
In-Reply-To
<CVN7HI3TH4QI.204OQUG27Q75K@ringo> (view parent)
DKIM signature
missing
Download raw message
On 19/09/2023, 23:19, Robin Jarry wrote:
> How about "show-thread-context" instead to match the style object name
> (see below)?
Wouldn't it be better "show-message-context"? Since the command really applies to messages and not threads and given that, in notmuch, a thread is an auto-contained object, "thread-context" sounds a bit weird to me.

Also, currently the command applies to all the listed messages in current folder/query: either we use "show-messages-context", in plural, or we change the behaviour of the command to apply to just the selected message. The later feels more natural to me, since one is generally focusing on a single thread when in need for drilling (maybe "drill" could be a short synonym?).

> The "borrowed" term feels weird. How about msglist_thread_context
> instead?
I'd say the same: msglist_message_context.

m2¢
Details
Message ID
<CVNST1R2SSP6.3M52YSYDCKLHT@timculverhouse.com>
In-Reply-To
<CVNJVW205TG4.3KGG8GMOH1P7H@sindominio.net> (view parent)
DKIM signature
missing
Download raw message
On Wed Sep 20, 2023 at 2:02 AM CDT, inwit wrote:
> On 19/09/2023, 23:19, Robin Jarry wrote:
> > How about "show-thread-context" instead to match the style object name (see
> > below)?
> Wouldn't it be better "show-message-context"? Since the command really applies
> to messages and not threads and given that, in notmuch, a thread is an
> auto-contained object, "thread-context" sounds a bit weird to me.

I prefer show-thread-context, this view only exists in a threaded view...this
is also more closely aligned to notmuch terminology ("show-entire-thread").

> Also, currently the command applies to all the listed messages in current
> folder/query: either we use "show-messages-context", in plural, or we change
> the behaviour of the command to apply to just the selected message. The later
> feels more natural to me, since one is generally focusing on a single thread
> when in need for drilling (maybe "drill" could be a short synonym?).

I still think "show-thread-context" is a better key. "show-thread-contexts"
if we wanted to pluralize. I think the command we should use if we implemented
showing a _single_ thread context is the same as notmuch: "show-entire-thread"

What do you think?

-- 
Tim
Details
Message ID
<CVQ7AGW2VE13.2J2EPZUTVQ49V@sindominio.net>
In-Reply-To
<CVNST1R2SSP6.3M52YSYDCKLHT@timculverhouse.com> (view parent)
DKIM signature
missing
Download raw message

On 20/09/2023, 16:01, Tim Culverhouse wrote:
> What do you think?
Not entirely convinced, but I'm fine with your proposal. Let's go ahead with this! :)
Reply to thread Export thread (mbox)