Compare commits

..

No commits in common. "14ceca320065656ea31994191f9e74d254a72e04" and "206665a2d97106722a6b32e24a504863040ca515" have entirely different histories.

11 changed files with 105 additions and 180 deletions

View file

@ -12,8 +12,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
`:rmdir`, `:archive` and the `copy-to` option. `:rmdir`, `:archive` and the `copy-to` option.
- Display messages from bottom to top with `reverse-msglist-order=true` in - Display messages from bottom to top with `reverse-msglist-order=true` in
`aerc.conf`. `aerc.conf`.
- Display threads from bottom to top with `reverse-thread-order=true` in
`aerc.conf`.
### Fixed ### Fixed

View file

@ -10,9 +10,7 @@ import (
"os/exec" "os/exec"
"regexp" "regexp"
"strings" "strings"
"syscall"
"git.sr.ht/~rjarry/aerc/logging"
"github.com/google/shlex" "github.com/google/shlex"
) )
@ -73,10 +71,6 @@ func isAddressHeader(h string) bool {
return false return false
} }
const maxCompletionLines = 100
var tooManyLines = fmt.Errorf("returned more than %d lines", maxCompletionLines)
// completeAddress uses the configured address book completion command to fetch // completeAddress uses the configured address book completion command to fetch
// completions for the specified string, returning a slice of completions and // completions for the specified string, returning a slice of completions and
// a prefix to be prepended to the selected completion, or an error. // a prefix to be prepended to the selected completion, or an error.
@ -94,8 +88,6 @@ func (c *Completer) completeAddress(s string) ([]string, string, error) {
if err != nil { if err != nil {
return nil, "", fmt.Errorf("stderr: %w", err) return nil, "", fmt.Errorf("stderr: %w", err)
} }
// reset the process group id to allow killing all its children
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return nil, "", fmt.Errorf("cmd start: %w", err) return nil, "", fmt.Errorf("cmd start: %w", err)
} }
@ -107,12 +99,6 @@ func (c *Completer) completeAddress(s string) ([]string, string, error) {
completions, err := readCompletions(stdout) completions, err := readCompletions(stdout)
if err != nil { if err != nil {
// make sure to kill the process *and* all its children
//nolint:errcheck // who cares?
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
logging.Warnf("command %s killed: %s", cmd, err)
}
if err != nil && !errors.Is(err, tooManyLines) {
buf, _ := io.ReadAll(stderr) buf, _ := io.ReadAll(stderr)
msg := strings.TrimSpace(string(buf)) msg := strings.TrimSpace(string(buf))
if msg != "" { if msg != "" {
@ -162,38 +148,27 @@ func (c *Completer) getAddressCmd(s string) (*exec.Cmd, error) {
func readCompletions(r io.Reader) ([]string, error) { func readCompletions(r io.Reader) ([]string, error) {
buf := bufio.NewReader(r) buf := bufio.NewReader(r)
completions := []string{} completions := []string{}
for i := 0; i < maxCompletionLines; i++ { for {
line, err := buf.ReadString('\n') line, err := buf.ReadString('\n')
if errors.Is(err, io.EOF) { if errors.Is(err, io.EOF) {
return completions, nil return completions, nil
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
if strings.TrimSpace(line) == "" {
// skip empty lines
continue
}
parts := strings.SplitN(line, "\t", 3) parts := strings.SplitN(line, "\t", 3)
addr, err := mail.ParseAddress(strings.TrimSpace(parts[0])) addr, err := mail.ParseAddress(strings.TrimSpace(parts[0]))
if err != nil { if err != nil {
logging.Warnf( return nil, err
"line %d: %#v: could not parse address: %v",
line, err)
continue
} }
if len(parts) > 1 { if len(parts) > 1 {
addr.Name = strings.TrimSpace(parts[1]) addr.Name = strings.TrimSpace(parts[1])
} }
decoded, err := decodeMIME(addr.String()) decoded, err := decodeMIME(addr.String())
if err != nil { if err != nil {
logging.Warnf( return nil, fmt.Errorf("could not decode MIME string: %w", err)
"line %d: %#v: could not decode MIME string: %v",
i+1, line, err)
continue
} }
completions = append(completions, decoded) completions = append(completions, decoded)
} }
return completions, tooManyLines
} }
func decodeMIME(s string) (string, error) { func decodeMIME(s string) (string, error) {

View file

@ -194,14 +194,6 @@ completion-popovers=true
# Default: false # Default: false
#reverse-msglist-order = false #reverse-msglist-order = false
# Reverse display of the mesage threads. Default order is the the intial
# message is on the top with all the replies being displayed below. The
# reverse option will put the initial message at the bottom with the
# replies on top.
#
# Default: false
#reverse-thread-order=false
#[ui:account=foo] #[ui:account=foo]
# #
# Enable a threaded view of messages. If this is not supported by the backend # Enable a threaded view of messages. If this is not supported by the backend

View file

@ -79,8 +79,7 @@ type UIConfig struct {
BorderCharVertical rune `ini:"-"` BorderCharVertical rune `ini:"-"`
BorderCharHorizontal rune `ini:"-"` BorderCharHorizontal rune `ini:"-"`
ReverseOrder bool `ini:"reverse-msglist-order"` ReverseOrder bool `ini:"reverse-msglist-order"`
ReverseThreadOrder bool `ini:"reverse-thread-order"`
} }
type ContextType int type ContextType int

View file

@ -335,14 +335,6 @@ These options are configured in the *[ui]* section of aerc.conf.
Default: false Default: false
*reverse-thread-order*
Reverse display of the mesage threads. Default order is the the intial
message is on the top with all the replies being displayed below. The
reverse option will put the initial message at the bottom with the
replies on top.
Default: false
*threading-enabled* *threading-enabled*
Enable a threaded view of messages. If this is not supported by the Enable a threaded view of messages. If this is not supported by the
backend (IMAP server or notmuch), threads will be built by the client. backend (IMAP server or notmuch), threads will be built by the client.

View file

@ -21,9 +21,6 @@ func FixBounds(i, lower, upper int) int {
// WrapBounds will wrap the index i around its upper- or lower-bound if // WrapBounds will wrap the index i around its upper- or lower-bound if
// out-of-bound // out-of-bound
func WrapBounds(i, lower, upper int) int { func WrapBounds(i, lower, upper int) int {
if upper <= 0 {
return lower
}
switch { switch {
case i > upper: case i > upper:
i = lower + (i-upper-1)%upper i = lower + (i-upper-1)%upper

View file

@ -39,10 +39,9 @@ type MessageStore struct {
sortCriteria []*types.SortCriterion sortCriteria []*types.SortCriterion
threadedView bool threadedView bool
reverseThreadOrder bool buildThreads bool
buildThreads bool builder *ThreadBuilder
builder *ThreadBuilder
// Map of uids we've asked the worker to fetch // Map of uids we've asked the worker to fetch
onUpdate func(store *MessageStore) // TODO: multiple onUpdate handlers onUpdate func(store *MessageStore) // TODO: multiple onUpdate handlers
@ -75,7 +74,7 @@ func NewMessageStore(worker *types.Worker,
dirInfo *models.DirectoryInfo, dirInfo *models.DirectoryInfo,
defaultSortCriteria []*types.SortCriterion, defaultSortCriteria []*types.SortCriterion,
thread bool, clientThreads bool, clientThreadsDelay time.Duration, thread bool, clientThreads bool, clientThreadsDelay time.Duration,
reverseOrder bool, reverseThreadOrder bool, reverseOrder bool,
triggerNewEmail func(*models.MessageInfo), triggerNewEmail func(*models.MessageInfo),
triggerDirectoryChange func(), triggerDirectoryChange func(),
) *MessageStore { ) *MessageStore {
@ -92,9 +91,8 @@ func NewMessageStore(worker *types.Worker,
bodyCallbacks: make(map[uint32][]func(*types.FullMessage)), bodyCallbacks: make(map[uint32][]func(*types.FullMessage)),
threadedView: thread, threadedView: thread,
buildThreads: clientThreads, buildThreads: clientThreads,
reverseThreadOrder: reverseThreadOrder,
filter: []string{"filter"}, filter: []string{"filter"},
sortCriteria: defaultSortCriteria, sortCriteria: defaultSortCriteria,
@ -233,7 +231,7 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
newMap := make(map[uint32]*models.MessageInfo) newMap := make(map[uint32]*models.MessageInfo)
builder := NewThreadBuilder(store.iterFactory) builder := NewThreadBuilder(store.iterFactory)
builder.RebuildUids(msg.Threads, store.reverseThreadOrder) builder.RebuildUids(msg.Threads)
store.uids = builder.Uids() store.uids = builder.Uids()
store.threads = msg.Threads store.threads = msg.Threads
@ -366,14 +364,6 @@ func (store *MessageStore) update(threads bool) {
} }
} }
func (store *MessageStore) SetReverseThreadOrder(reverse bool) {
store.reverseThreadOrder = reverse
}
func (store *MessageStore) ReverseThreadOrder() bool {
return store.reverseThreadOrder
}
func (store *MessageStore) SetThreadedView(thread bool) { func (store *MessageStore) SetThreadedView(thread bool) {
store.threadedView = thread store.threadedView = thread
if store.buildThreads { if store.buildThreads {
@ -434,7 +424,7 @@ func (store *MessageStore) runThreadBuilderNow() {
} }
} }
// build new threads // build new threads
th := store.builder.Threads(store.uids, store.reverseThreadOrder) th := store.builder.Threads(store.uids)
// save local threads to the message store variable and // save local threads to the message store variable and
// run callback if defined (callback should reposition cursor) // run callback if defined (callback should reposition cursor)
@ -603,20 +593,29 @@ func (store *MessageStore) NextPrev(delta int) {
if len(uids) == 0 { if len(uids) == 0 {
return return
} }
iter := store.iterFactory.NewIterator(uids) iter := store.iterFactory.NewIterator(uids)
newIdx := store.FindIndexByUid(store.SelectedUid()) uid := store.SelectedUid()
newIdx := store.FindIndexByUid(uid)
if newIdx < 0 { if newIdx < 0 {
store.Select(uids[iter.StartIndex()]) store.Select(uids[iter.StartIndex()])
return return
} }
newIdx = iterator.MoveIndex(
newIdx, low, high := iter.EndIndex(), iter.StartIndex()
delta, sign := -1
iter, if high < low {
iterator.FixBounds, low, high = high, low
) sign = 1
}
newIdx += sign * delta
if newIdx >= len(uids) {
newIdx = high
} else if newIdx < 0 {
newIdx = low
}
store.Select(uids[newIdx]) store.Select(uids[newIdx])
if store.BuildThreads() && store.ThreadedView() { if store.BuildThreads() && store.ThreadedView() {
@ -632,7 +631,15 @@ func (store *MessageStore) NextPrev(delta int) {
if store.marker != nil { if store.marker != nil {
store.marker.UpdateVisualMark() store.marker.UpdateVisualMark()
} }
store.updateResults()
nextResultIndex := len(store.results) - store.resultIndex - 2*delta
if nextResultIndex < 0 || nextResultIndex >= len(store.results) {
return
}
nextResultUid := store.results[nextResultIndex]
if nextResultUid == store.SelectedUid() {
store.resultIndex += delta
}
} }
func (store *MessageStore) Next() { func (store *MessageStore) Next() {
@ -683,35 +690,18 @@ func (store *MessageStore) ApplyClear() {
store.Sort(nil, nil) store.Sort(nil, nil)
} }
func (store *MessageStore) updateResults() {
if len(store.results) == 0 || store.resultIndex < 0 {
return
}
uid := store.SelectedUid()
for i, u := range store.results {
if uid == u {
store.resultIndex = i
break
}
}
}
func (store *MessageStore) nextPrevResult(delta int) { func (store *MessageStore) nextPrevResult(delta int) {
if len(store.results) == 0 { if len(store.results) == 0 {
return return
} }
iter := store.iterFactory.NewIterator(store.results) store.resultIndex += delta
if store.resultIndex < 0 { if store.resultIndex >= len(store.results) {
store.resultIndex = iter.StartIndex() store.resultIndex = 0
} else {
store.resultIndex = iterator.MoveIndex(
store.resultIndex,
delta,
iter,
iterator.WrapBounds,
)
} }
store.Select(store.results[store.resultIndex]) if store.resultIndex < 0 {
store.resultIndex = len(store.results) - 1
}
store.Select(store.results[len(store.results)-store.resultIndex-1])
store.update(false) store.update(false)
} }

View file

@ -55,7 +55,7 @@ func (builder *ThreadBuilder) Update(msg *models.MessageInfo) {
} }
// Threads returns a slice of threads for the given list of uids // Threads returns a slice of threads for the given list of uids
func (builder *ThreadBuilder) Threads(uids []uint32, inverse bool) []*types.Thread { func (builder *ThreadBuilder) Threads(uids []uint32) []*types.Thread {
builder.Lock() builder.Lock()
defer builder.Unlock() defer builder.Unlock()
@ -67,7 +67,7 @@ func (builder *ThreadBuilder) Threads(uids []uint32, inverse bool) []*types.Thre
builder.sortThreads(threads, uids) builder.sortThreads(threads, uids)
// rebuild uids from threads // rebuild uids from threads
builder.RebuildUids(threads, inverse) builder.RebuildUids(threads)
elapsed := time.Since(start) elapsed := time.Since(start)
logging.Infof("%d threads created in %s", len(threads), elapsed) logging.Infof("%d threads created in %s", len(threads), elapsed)
@ -155,23 +155,15 @@ func (builder *ThreadBuilder) sortThreads(threads []*types.Thread, orderedUids [
} }
// RebuildUids rebuilds the uids from the given slice of threads // RebuildUids rebuilds the uids from the given slice of threads
func (builder *ThreadBuilder) RebuildUids(threads []*types.Thread, inverse bool) { func (builder *ThreadBuilder) RebuildUids(threads []*types.Thread) {
uids := make([]uint32, 0, len(threads)) uids := make([]uint32, 0, len(threads))
iterT := builder.iterFactory.NewIterator(threads) iterT := builder.iterFactory.NewIterator(threads)
for iterT.Next() { for iterT.Next() {
var threaduids []uint32
_ = iterT.Value().(*types.Thread).Walk( _ = iterT.Value().(*types.Thread).Walk(
func(t *types.Thread, level int, currentErr error) error { func(t *types.Thread, level int, currentErr error) error {
threaduids = append(threaduids, t.Uid) uids = append(uids, t.Uid)
return nil return nil
}) })
if inverse {
for j := len(threaduids) - 1; j >= 0; j-- {
uids = append(uids, threaduids[j])
}
} else {
uids = append(uids, threaduids...)
}
} }
result := make([]uint32, 0, len(uids)) result := make([]uint32, 0, len(uids))
iterU := builder.iterFactory.NewIterator(uids) iterU := builder.iterFactory.NewIterator(uids)

View file

@ -286,7 +286,6 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
acct.dirlist.UiConfig(name).ForceClientThreads, acct.dirlist.UiConfig(name).ForceClientThreads,
acct.dirlist.UiConfig(name).ClientThreadsDelay, acct.dirlist.UiConfig(name).ClientThreadsDelay,
acct.dirlist.UiConfig(name).ReverseOrder, acct.dirlist.UiConfig(name).ReverseOrder,
acct.dirlist.UiConfig(name).ReverseThreadOrder,
func(msg *models.MessageInfo) { func(msg *models.MessageInfo) {
acct.conf.Triggers.ExecNewEmail(acct.acct, acct.conf.Triggers.ExecNewEmail(acct.acct,
acct.conf, msg) acct.conf, msg)

View file

@ -249,7 +249,7 @@ func (dt *DirectoryTree) countVisible(list []*types.Thread) (n int) {
func (dt *DirectoryTree) displayText(node *types.Thread) string { func (dt *DirectoryTree) displayText(node *types.Thread) string {
elems := strings.Split(dt.treeDirs[getAnyUid(node)], dt.pathSeparator) elems := strings.Split(dt.treeDirs[getAnyUid(node)], dt.pathSeparator)
return fmt.Sprintf("%s%s%s", threadPrefix(node, false), getFlag(node), elems[countLevels(node)]) return fmt.Sprintf("%s%s%s", threadPrefix(node), getFlag(node), elems[countLevels(node)])
} }
func (dt *DirectoryTree) getDirectory(node *types.Thread) string { func (dt *DirectoryTree) getDirectory(node *types.Thread) string {

View file

@ -12,7 +12,6 @@ import (
"git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/format" "git.sr.ht/~rjarry/aerc/lib/format"
"git.sr.ht/~rjarry/aerc/lib/iterator"
"git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/logging" "git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/models"
@ -89,61 +88,57 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
row int = 0 row int = 0
) )
createBaseCtx := func(uid uint32, row int) format.Ctx {
return format.Ctx{
FromAddress: acct.acct.From,
AccountName: acct.Name(),
MsgInfo: store.Messages[uid],
MsgNum: row,
MsgIsMarked: store.Marker().IsMarked(uid),
}
}
if store.ThreadedView() { if store.ThreadedView() {
var ( iter := store.ThreadsIterator()
lastSubject string var i int = 0
prevThread *types.Thread
i int = 0 for iter.Next() {
) thread := iter.Value().(*types.Thread)
factory := iterator.NewFactory(!store.ReverseThreadOrder()) var lastSubject string
threadLoop: err := thread.Walk(func(t *types.Thread, _ int, currentErr error) error {
for iter := store.ThreadsIterator(); iter.Next(); { if currentErr != nil {
var cur []*types.Thread return currentErr
err := iter.Value().(*types.Thread).Walk( }
func(t *types.Thread, _ int, _ error, if t.Hidden || t.Deleted {
) error {
if t.Hidden || t.Deleted {
return nil
}
cur = append(cur, t)
return nil return nil
}) }
if err != nil {
logging.Errorf("thread walk: %v", err)
}
for curIter := factory.NewIterator(cur); curIter.Next(); {
if i < ml.Scroll() { if i < ml.Scroll() {
i++ i++
continue return nil
} }
if thread := curIter.Value().(*types.Thread); thread != nil { i++
fmtCtx := createBaseCtx(thread.Uid, row) msg := store.Messages[t.Uid]
fmtCtx.ThreadPrefix = threadPrefix(thread, var prefix string
store.ReverseThreadOrder()) var subject string
if fmtCtx.MsgInfo != nil && fmtCtx.MsgInfo.Envelope != nil { var normalizedSubject string
baseSubject, _ := sortthread.GetBaseSubject( if msg != nil {
fmtCtx.MsgInfo.Envelope.Subject) prefix = threadPrefix(t)
fmtCtx.ThreadSameSubject = baseSubject == lastSubject && if msg.Envelope != nil {
sameParent(thread, prevThread) && subject = msg.Envelope.Subject
!isParent(thread) normalizedSubject, _ = sortthread.GetBaseSubject(subject)
lastSubject = baseSubject
prevThread = thread
} }
if ml.drawRow(textWidth, ctx, thread.Uid, row, &needsHeaders, fmtCtx) {
break threadLoop
}
row += 1
} }
fmtCtx := format.Ctx{
FromAddress: acct.acct.From,
AccountName: acct.Name(),
MsgInfo: msg,
MsgNum: row,
MsgIsMarked: store.Marker().IsMarked(t.Uid),
ThreadPrefix: prefix,
ThreadSameSubject: normalizedSubject == lastSubject,
}
if ml.drawRow(textWidth, ctx, t.Uid, row, &needsHeaders, fmtCtx) {
return types.ErrSkipThread
}
lastSubject = normalizedSubject
row++
return nil
})
if err != nil {
logging.Warnf("failed to walk threads: %v", err)
}
if row >= ctx.Height() {
break
} }
} }
} else { } else {
@ -153,7 +148,15 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
continue continue
} }
uid := iter.Value().(uint32) uid := iter.Value().(uint32)
fmtCtx := createBaseCtx(uid, row)
msg := store.Messages[uid]
fmtCtx := format.Ctx{
FromAddress: acct.acct.From,
AccountName: acct.Name(),
MsgInfo: msg,
MsgNum: row,
MsgIsMarked: store.Marker().IsMarked(uid),
}
if ml.drawRow(textWidth, ctx, uid, row, &needsHeaders, fmtCtx) { if ml.drawRow(textWidth, ctx, uid, row, &needsHeaders, fmtCtx) {
break break
} }
@ -426,17 +429,13 @@ func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) {
uiConfig.GetStyle(config.STYLE_MSGLIST_DEFAULT), "%s", msg) uiConfig.GetStyle(config.STYLE_MSGLIST_DEFAULT), "%s", msg)
} }
func threadPrefix(t *types.Thread, reverse bool) string { func threadPrefix(t *types.Thread) string {
var arrow string var arrow string
if t.Parent != nil { if t.Parent != nil {
if t.NextSibling != nil { if t.NextSibling != nil {
arrow = "├─>" arrow = "├─>"
} else { } else {
if reverse { arrow = "└─>"
arrow = "┌─>"
} else {
arrow = "└─>"
}
} }
} }
var prefix []string var prefix []string
@ -459,11 +458,3 @@ func threadPrefix(t *types.Thread, reverse bool) string {
ps := strings.Join(prefix, "") ps := strings.Join(prefix, "")
return fmt.Sprintf("%v%v", ps, arrow) return fmt.Sprintf("%v%v", ps, arrow)
} }
func sameParent(left, right *types.Thread) bool {
return left.Root() == right.Root()
}
func isParent(t *types.Thread) bool {
return t == t.Root()
}