init: pristine aerc 0.20.0 source
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/config"
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
"github.com/emersion/go-message/mail"
|
||||
)
|
||||
|
||||
type WorkerMessage interface {
|
||||
InResponseTo() WorkerMessage
|
||||
getId() int64
|
||||
setId(id int64)
|
||||
Account() string
|
||||
setAccount(string)
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
inResponseTo WorkerMessage
|
||||
id int64
|
||||
acct string
|
||||
}
|
||||
|
||||
func RespondTo(msg WorkerMessage) Message {
|
||||
return Message{
|
||||
inResponseTo: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (m Message) InResponseTo() WorkerMessage {
|
||||
return m.inResponseTo
|
||||
}
|
||||
|
||||
func (m Message) getId() int64 {
|
||||
return m.id
|
||||
}
|
||||
|
||||
func (m *Message) setId(id int64) {
|
||||
m.id = id
|
||||
}
|
||||
|
||||
func (m *Message) Account() string {
|
||||
return m.acct
|
||||
}
|
||||
|
||||
func (m *Message) setAccount(name string) {
|
||||
m.acct = name
|
||||
}
|
||||
|
||||
// Meta-messages
|
||||
|
||||
type Done struct {
|
||||
Message
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Message
|
||||
Error error
|
||||
}
|
||||
|
||||
type Cancelled struct {
|
||||
Message
|
||||
}
|
||||
|
||||
type ConnError struct {
|
||||
Message
|
||||
Error error
|
||||
}
|
||||
|
||||
type Unsupported struct {
|
||||
Message
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
type Configure struct {
|
||||
Message
|
||||
Config *config.AccountConfig
|
||||
}
|
||||
|
||||
type Connect struct {
|
||||
Message
|
||||
}
|
||||
|
||||
type Reconnect struct {
|
||||
Message
|
||||
}
|
||||
|
||||
type Disconnect struct {
|
||||
Message
|
||||
}
|
||||
|
||||
type ListDirectories struct {
|
||||
Message
|
||||
}
|
||||
|
||||
type OpenDirectory struct {
|
||||
Message
|
||||
Context context.Context
|
||||
Directory string
|
||||
Query string
|
||||
Force bool
|
||||
}
|
||||
|
||||
type FetchDirectoryContents struct {
|
||||
Message
|
||||
Context context.Context
|
||||
SortCriteria []*SortCriterion
|
||||
Filter *SearchCriteria
|
||||
}
|
||||
|
||||
type FetchDirectoryThreaded struct {
|
||||
Message
|
||||
Context context.Context
|
||||
SortCriteria []*SortCriterion
|
||||
Filter *SearchCriteria
|
||||
ThreadContext bool
|
||||
}
|
||||
|
||||
type SearchDirectory struct {
|
||||
Message
|
||||
Context context.Context
|
||||
Criteria *SearchCriteria
|
||||
}
|
||||
|
||||
type DirectoryThreaded struct {
|
||||
Message
|
||||
Threads []*Thread
|
||||
}
|
||||
|
||||
type CreateDirectory struct {
|
||||
Message
|
||||
Directory string
|
||||
Quiet bool
|
||||
}
|
||||
|
||||
type RemoveDirectory struct {
|
||||
Message
|
||||
Directory string
|
||||
Quiet bool
|
||||
}
|
||||
|
||||
type FetchMessageHeaders struct {
|
||||
Message
|
||||
Context context.Context
|
||||
Uids []models.UID
|
||||
}
|
||||
|
||||
type FetchFullMessages struct {
|
||||
Message
|
||||
Uids []models.UID
|
||||
}
|
||||
|
||||
type FetchMessageBodyPart struct {
|
||||
Message
|
||||
Uid models.UID
|
||||
Part []int
|
||||
}
|
||||
|
||||
type FetchMessageFlags struct {
|
||||
Message
|
||||
Context context.Context
|
||||
Uids []models.UID
|
||||
}
|
||||
|
||||
type DeleteMessages struct {
|
||||
Message
|
||||
Uids []models.UID
|
||||
MultiFileStrategy *MultiFileStrategy
|
||||
}
|
||||
|
||||
// Flag messages with different mail types
|
||||
type FlagMessages struct {
|
||||
Message
|
||||
Enable bool
|
||||
Flags models.Flags
|
||||
Uids []models.UID
|
||||
}
|
||||
|
||||
type AnsweredMessages struct {
|
||||
Message
|
||||
Answered bool
|
||||
Uids []models.UID
|
||||
}
|
||||
|
||||
type ForwardedMessages struct {
|
||||
Message
|
||||
Forwarded bool
|
||||
Uids []models.UID
|
||||
}
|
||||
|
||||
type CopyMessages struct {
|
||||
Message
|
||||
Destination string
|
||||
Uids []models.UID
|
||||
MultiFileStrategy *MultiFileStrategy
|
||||
}
|
||||
|
||||
type MoveMessages struct {
|
||||
Message
|
||||
Destination string
|
||||
Uids []models.UID
|
||||
MultiFileStrategy *MultiFileStrategy
|
||||
}
|
||||
|
||||
type AppendMessage struct {
|
||||
Message
|
||||
Destination string
|
||||
Flags models.Flags
|
||||
Date time.Time
|
||||
Reader io.Reader
|
||||
Length int
|
||||
}
|
||||
|
||||
type CheckMail struct {
|
||||
Message
|
||||
Directories []string
|
||||
Command string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type StartSendingMessage struct {
|
||||
Message
|
||||
From *mail.Address
|
||||
Rcpts []*mail.Address
|
||||
CopyTo []string
|
||||
}
|
||||
|
||||
// Messages
|
||||
|
||||
type Directory struct {
|
||||
Message
|
||||
Dir *models.Directory
|
||||
}
|
||||
|
||||
type DirectoryInfo struct {
|
||||
Message
|
||||
Info *models.DirectoryInfo
|
||||
Refetch bool
|
||||
}
|
||||
|
||||
type DirectoryContents struct {
|
||||
Message
|
||||
Uids []models.UID
|
||||
}
|
||||
|
||||
type SearchResults struct {
|
||||
Message
|
||||
Uids []models.UID
|
||||
}
|
||||
|
||||
type MessageInfo struct {
|
||||
Message
|
||||
Info *models.MessageInfo
|
||||
NeedsFlags bool
|
||||
}
|
||||
|
||||
type FullMessage struct {
|
||||
Message
|
||||
Content *models.FullMessage
|
||||
}
|
||||
|
||||
type MessageBodyPart struct {
|
||||
Message
|
||||
Part *models.MessageBodyPart
|
||||
}
|
||||
|
||||
type MessagesDeleted struct {
|
||||
Message
|
||||
Uids []models.UID
|
||||
}
|
||||
|
||||
type MessagesCopied struct {
|
||||
Message
|
||||
Destination string
|
||||
Uids []models.UID
|
||||
}
|
||||
|
||||
type MessagesMoved struct {
|
||||
Message
|
||||
Destination string
|
||||
Uids []models.UID
|
||||
}
|
||||
|
||||
type ModifyLabels struct {
|
||||
Message
|
||||
Uids []models.UID
|
||||
Add []string
|
||||
Remove []string
|
||||
}
|
||||
|
||||
type LabelList struct {
|
||||
Message
|
||||
Labels []string
|
||||
}
|
||||
|
||||
type CheckMailDirectories struct {
|
||||
Message
|
||||
Directories []string
|
||||
}
|
||||
|
||||
type MessageWriter struct {
|
||||
Message
|
||||
Writer io.WriteCloser
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package types
|
||||
|
||||
// MultiFileStrategy represents a strategy for taking file-based actions (e.g.,
|
||||
// move, copy, delete) on messages that are represented by more than one file.
|
||||
// These strategies are only used by the notmuch backend but are defined in this
|
||||
// package to prevent import cycles.
|
||||
type MultiFileStrategy uint
|
||||
|
||||
const (
|
||||
Refuse MultiFileStrategy = iota
|
||||
ActAll
|
||||
ActOne
|
||||
ActOneDelRest
|
||||
ActDir
|
||||
ActDirDelRest
|
||||
)
|
||||
|
||||
var StrToStrategy = map[string]MultiFileStrategy{
|
||||
"refuse": Refuse,
|
||||
"act-all": ActAll,
|
||||
"act-one": ActOne,
|
||||
"act-one-delete-rest": ActOneDelRest,
|
||||
"act-dir": ActDir,
|
||||
"act-dir-delete-rest": ActDirDelRest,
|
||||
}
|
||||
|
||||
func StrategyStrs() []string {
|
||||
strs := make([]string, 0, len(StrToStrategy))
|
||||
for s := range StrToStrategy {
|
||||
strs = append(strs, s)
|
||||
}
|
||||
return strs
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"net/textproto"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
)
|
||||
|
||||
type SearchCriteria struct {
|
||||
WithFlags models.Flags
|
||||
WithoutFlags models.Flags
|
||||
From []string
|
||||
To []string
|
||||
Cc []string
|
||||
Headers textproto.MIMEHeader
|
||||
StartDate time.Time
|
||||
EndDate time.Time
|
||||
SearchBody bool
|
||||
SearchAll bool
|
||||
Terms []string
|
||||
UseExtension bool
|
||||
}
|
||||
|
||||
func (c *SearchCriteria) PrepareHeader() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
if c.Headers == nil {
|
||||
c.Headers = make(textproto.MIMEHeader)
|
||||
}
|
||||
for _, from := range c.From {
|
||||
c.Headers.Add("From", from)
|
||||
}
|
||||
for _, to := range c.To {
|
||||
c.Headers.Add("To", to)
|
||||
}
|
||||
for _, cc := range c.Cc {
|
||||
c.Headers.Add("Cc", cc)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SearchCriteria) Combine(other *SearchCriteria) *SearchCriteria {
|
||||
if c == nil {
|
||||
return other
|
||||
}
|
||||
headers := make(textproto.MIMEHeader)
|
||||
for k, v := range c.Headers {
|
||||
headers[k] = v
|
||||
}
|
||||
for k, v := range other.Headers {
|
||||
headers[k] = v
|
||||
}
|
||||
start := c.StartDate
|
||||
if !other.StartDate.IsZero() {
|
||||
start = other.StartDate
|
||||
}
|
||||
end := c.EndDate
|
||||
if !other.EndDate.IsZero() {
|
||||
end = other.EndDate
|
||||
}
|
||||
from := make([]string, len(c.From)+len(other.From))
|
||||
copy(from[:len(c.From)], c.From)
|
||||
copy(from[len(c.From):], other.From)
|
||||
to := make([]string, len(c.To)+len(other.To))
|
||||
copy(to[:len(c.To)], c.To)
|
||||
copy(to[len(c.To):], other.To)
|
||||
cc := make([]string, len(c.Cc)+len(other.Cc))
|
||||
copy(cc[:len(c.Cc)], c.Cc)
|
||||
copy(cc[len(c.Cc):], other.Cc)
|
||||
return &SearchCriteria{
|
||||
WithFlags: c.WithFlags | other.WithFlags,
|
||||
WithoutFlags: c.WithoutFlags | other.WithoutFlags,
|
||||
From: from,
|
||||
To: to,
|
||||
Cc: cc,
|
||||
Headers: headers,
|
||||
StartDate: start,
|
||||
EndDate: end,
|
||||
SearchBody: c.SearchBody || other.SearchBody,
|
||||
SearchAll: c.SearchAll || other.SearchAll,
|
||||
Terms: append(c.Terms, other.Terms...),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package types
|
||||
|
||||
type SortField int
|
||||
|
||||
const (
|
||||
SortArrival SortField = iota
|
||||
SortCc
|
||||
SortDate
|
||||
SortFrom
|
||||
SortRead
|
||||
SortSize
|
||||
SortSubject
|
||||
SortTo
|
||||
SortFlagged
|
||||
)
|
||||
|
||||
type SortCriterion struct {
|
||||
Field SortField
|
||||
Reverse bool
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
)
|
||||
|
||||
type Thread struct {
|
||||
Uid models.UID
|
||||
Parent *Thread
|
||||
PrevSibling *Thread
|
||||
NextSibling *Thread
|
||||
FirstChild *Thread
|
||||
|
||||
Hidden int // if this flag is not zero the message isn't rendered in the UI
|
||||
Deleted bool // if this flag is set the message was deleted
|
||||
|
||||
// if this flag is set the message is the root of an incomplete thread
|
||||
Dummy bool
|
||||
|
||||
// Context indicates the message doesn't match the mailbox / query but
|
||||
// is displayed for context
|
||||
Context bool
|
||||
}
|
||||
|
||||
// AddChild appends the child node at the end of the existing children of t.
|
||||
func (t *Thread) AddChild(child *Thread) {
|
||||
t.InsertCmp(child, func(_, _ *Thread) bool { return true })
|
||||
}
|
||||
|
||||
// OrderedInsert inserts the child node in ascending order among the existing
|
||||
// children based on their respective UIDs.
|
||||
func (t *Thread) OrderedInsert(child *Thread) {
|
||||
t.InsertCmp(child, func(child, iter *Thread) bool { return child.Uid > iter.Uid })
|
||||
}
|
||||
|
||||
// InsertCmp inserts child as a child node into t in ascending order. The
|
||||
// ascending order is determined by the bigger function that compares the child
|
||||
// with the existing children. It should return true when the child is bigger
|
||||
// than the other, and false otherwise.
|
||||
func (t *Thread) InsertCmp(child *Thread, bigger func(*Thread, *Thread) bool) {
|
||||
if t.FirstChild == nil {
|
||||
t.FirstChild = child
|
||||
} else {
|
||||
start := &Thread{NextSibling: t.FirstChild}
|
||||
var iter *Thread
|
||||
for iter = start; iter.NextSibling != nil &&
|
||||
bigger(child, iter.NextSibling); iter = iter.NextSibling {
|
||||
}
|
||||
child.NextSibling = iter.NextSibling
|
||||
iter.NextSibling = child
|
||||
t.FirstChild = start.NextSibling
|
||||
}
|
||||
child.Parent = t
|
||||
}
|
||||
|
||||
func (t *Thread) Walk(walkFn NewThreadWalkFn) error {
|
||||
err := newWalk(t, walkFn, 0, nil)
|
||||
if errors.Is(err, ErrSkipThread) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Root returns the root thread of the thread tree
|
||||
func (t *Thread) Root() *Thread {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
var iter *Thread
|
||||
for iter = t; iter.Parent != nil; iter = iter.Parent {
|
||||
}
|
||||
return iter
|
||||
}
|
||||
|
||||
// Uids returns all associated uids for the given thread and its children
|
||||
func (t *Thread) Uids() []models.UID {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
uids := make([]models.UID, 0)
|
||||
err := t.Walk(func(node *Thread, _ int, _ error) error {
|
||||
uids = append(uids, node.Uid)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("walk to collect uids failed: %v", err)
|
||||
}
|
||||
return uids
|
||||
}
|
||||
|
||||
func (t *Thread) String() string {
|
||||
if t == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
var parent models.UID
|
||||
if t.Parent != nil {
|
||||
parent = t.Parent.Uid
|
||||
}
|
||||
var next models.UID
|
||||
if t.NextSibling != nil {
|
||||
next = t.NextSibling.Uid
|
||||
}
|
||||
var child models.UID
|
||||
if t.FirstChild != nil {
|
||||
child = t.FirstChild.Uid
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"[%s] (parent:%s, next:%s, child:%s)",
|
||||
t.Uid, parent, next, child,
|
||||
)
|
||||
}
|
||||
|
||||
func newWalk(node *Thread, walkFn NewThreadWalkFn, lvl int, ce error) error {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
err := walkFn(node, lvl, ce)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
err = newWalk(child, walkFn, lvl+1, err)
|
||||
if errors.Is(err, ErrSkipThread) {
|
||||
err = nil
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var ErrSkipThread = errors.New("skip this Thread")
|
||||
|
||||
type NewThreadWalkFn func(t *Thread, level int, currentErr error) error
|
||||
|
||||
// Implement interface to be able to sort threads by newest (max UID)
|
||||
type ByUID []*Thread
|
||||
|
||||
func getMaxUID(thread *Thread) models.UID {
|
||||
// TODO: should we make this part of the Thread type to avoid recomputation?
|
||||
var Uid models.UID
|
||||
|
||||
_ = thread.Walk(func(t *Thread, _ int, currentErr error) error {
|
||||
if t.Deleted || t.Hidden > 0 {
|
||||
return nil
|
||||
}
|
||||
if t.Uid > Uid {
|
||||
Uid = t.Uid
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return Uid
|
||||
}
|
||||
|
||||
func (s ByUID) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s ByUID) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
func (s ByUID) Less(i, j int) bool {
|
||||
maxUID_i := getMaxUID(s[i])
|
||||
maxUID_j := getMaxUID(s[j])
|
||||
return maxUID_i < maxUID_j
|
||||
}
|
||||
|
||||
func SortThreadsBy(toSort []*Thread, sortBy []models.UID) {
|
||||
// build a map from sortBy
|
||||
uidMap := make(map[models.UID]int)
|
||||
for i, uid := range sortBy {
|
||||
uidMap[uid] = i
|
||||
}
|
||||
// sortslice of toSort with less function of indexing the map sortBy
|
||||
sort.Slice(toSort, func(i, j int) bool {
|
||||
return uidMap[getMaxUID(toSort[i])] < uidMap[getMaxUID(toSort[j])]
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
)
|
||||
|
||||
func genFakeTree() *Thread {
|
||||
tree := new(Thread)
|
||||
var prevChild *Thread
|
||||
for i := uint32(1); i < uint32(3); i++ {
|
||||
child := &Thread{
|
||||
Uid: models.Uint32ToUid(i * 10),
|
||||
Parent: tree,
|
||||
PrevSibling: prevChild,
|
||||
}
|
||||
if prevChild != nil {
|
||||
prevChild.NextSibling = child
|
||||
} else if tree.FirstChild == nil {
|
||||
tree.FirstChild = child
|
||||
} else {
|
||||
panic("unreachable")
|
||||
}
|
||||
prevChild = child
|
||||
var prevSecond *Thread
|
||||
for j := uint32(1); j < uint32(3); j++ {
|
||||
second := &Thread{
|
||||
Uid: models.Uint32ToUid(models.UidToUint32(child.Uid) + j),
|
||||
Parent: child,
|
||||
PrevSibling: prevSecond,
|
||||
}
|
||||
if prevSecond != nil {
|
||||
prevSecond.NextSibling = second
|
||||
} else if child.FirstChild == nil {
|
||||
child.FirstChild = second
|
||||
} else {
|
||||
panic("unreachable")
|
||||
}
|
||||
prevSecond = second
|
||||
var prevThird *Thread
|
||||
limit := uint32(3)
|
||||
if j == 2 {
|
||||
limit = 8
|
||||
}
|
||||
for k := uint32(1); k < limit; k++ {
|
||||
third := &Thread{
|
||||
Uid: models.Uint32ToUid(models.UidToUint32(second.Uid)*10 + j),
|
||||
Parent: second,
|
||||
PrevSibling: prevThird,
|
||||
}
|
||||
if prevThird != nil {
|
||||
prevThird.NextSibling = third
|
||||
} else if second.FirstChild == nil {
|
||||
second.FirstChild = third
|
||||
} else {
|
||||
panic("unreachable")
|
||||
}
|
||||
prevThird = third
|
||||
}
|
||||
}
|
||||
}
|
||||
return tree
|
||||
}
|
||||
|
||||
func TestNewWalk(t *testing.T) {
|
||||
tree := genFakeTree()
|
||||
var prefix []string
|
||||
lastLevel := 0
|
||||
tree.Walk(func(t *Thread, lvl int, e error) error {
|
||||
if e != nil {
|
||||
fmt.Printf("ERROR: %v\n", e)
|
||||
}
|
||||
if lvl > lastLevel && lvl > 1 {
|
||||
// we actually just descended... so figure out what connector we need
|
||||
// level 1 is flush to the root, so we avoid the indentation there
|
||||
if t.Parent.NextSibling != nil {
|
||||
prefix = append(prefix, "│ ")
|
||||
} else {
|
||||
prefix = append(prefix, " ")
|
||||
}
|
||||
} else if lvl < lastLevel {
|
||||
// ascended, need to trim the prefix layers
|
||||
diff := lastLevel - lvl
|
||||
prefix = prefix[:len(prefix)-diff]
|
||||
}
|
||||
|
||||
var arrow string
|
||||
if t.Parent != nil {
|
||||
if t.NextSibling != nil {
|
||||
arrow = "├─>"
|
||||
} else {
|
||||
arrow = "└─>"
|
||||
}
|
||||
}
|
||||
|
||||
// format
|
||||
fmt.Printf("%s%s%s\n", strings.Join(prefix, ""), arrow, t)
|
||||
|
||||
lastLevel = lvl
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func uidSeq(tree *Thread) string {
|
||||
var seq []string
|
||||
tree.Walk(func(t *Thread, _ int, _ error) error {
|
||||
seq = append(seq, string(t.Uid))
|
||||
return nil
|
||||
})
|
||||
return strings.Join(seq, ".")
|
||||
}
|
||||
|
||||
func TestThread_AddChild(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
seq []models.UID
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "ascending",
|
||||
seq: []models.UID{"1", "2", "3", "4", "5", "6"},
|
||||
want: ".1.2.3.4.5.6",
|
||||
},
|
||||
{
|
||||
name: "descending",
|
||||
seq: []models.UID{"6", "5", "4", "3", "2", "1"},
|
||||
want: ".6.5.4.3.2.1",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
tree := new(Thread)
|
||||
for _, i := range test.seq {
|
||||
tree.AddChild(&Thread{Uid: i})
|
||||
}
|
||||
if got := uidSeq(tree); got != test.want {
|
||||
t.Errorf("got: %s, but wanted: %s", got,
|
||||
test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestThread_OrderedInsert(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
seq []models.UID
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "ascending",
|
||||
seq: []models.UID{"1", "2", "3", "4", "5", "6"},
|
||||
want: ".1.2.3.4.5.6",
|
||||
},
|
||||
{
|
||||
name: "descending",
|
||||
seq: []models.UID{"6", "5", "4", "3", "2", "1"},
|
||||
want: ".1.2.3.4.5.6",
|
||||
},
|
||||
{
|
||||
name: "mixed",
|
||||
seq: []models.UID{"2", "1", "6", "3", "4", "5"},
|
||||
want: ".1.2.3.4.5.6",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
tree := new(Thread)
|
||||
for _, i := range test.seq {
|
||||
tree.OrderedInsert(&Thread{Uid: i})
|
||||
}
|
||||
if got := uidSeq(tree); got != test.want {
|
||||
t.Errorf("got: %s, but wanted: %s", got,
|
||||
test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestThread_InsertCmd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
seq []models.UID
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "ascending",
|
||||
seq: []models.UID{"1", "2", "3", "4", "5", "6"},
|
||||
want: ".6.4.2.1.3.5",
|
||||
},
|
||||
{
|
||||
name: "descending",
|
||||
seq: []models.UID{"6", "5", "4", "3", "2", "1"},
|
||||
want: ".6.4.2.1.3.5",
|
||||
},
|
||||
{
|
||||
name: "mixed",
|
||||
seq: []models.UID{"2", "1", "6", "3", "4", "5"},
|
||||
want: ".6.4.2.1.3.5",
|
||||
},
|
||||
}
|
||||
sortMap := map[models.UID]int{
|
||||
"6": 1,
|
||||
"4": 2,
|
||||
"2": 3,
|
||||
"1": 4,
|
||||
"3": 5,
|
||||
"5": 6,
|
||||
}
|
||||
|
||||
// bigger compares the new child with the next node and returns true if
|
||||
// the child node is bigger and false otherwise.
|
||||
bigger := func(newNode, nextChild *Thread) bool {
|
||||
return sortMap[newNode.Uid] > sortMap[nextChild.Uid]
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
tree := new(Thread)
|
||||
for _, i := range test.seq {
|
||||
tree.InsertCmp(&Thread{Uid: i}, bigger)
|
||||
}
|
||||
if got := uidSeq(tree); got != test.want {
|
||||
t.Errorf("got: %s, but wanted: %s", got,
|
||||
test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
)
|
||||
|
||||
type WorkerInteractor interface {
|
||||
log.Logger
|
||||
Actions() chan WorkerMessage
|
||||
ProcessAction(WorkerMessage) WorkerMessage
|
||||
PostAction(WorkerMessage, func(msg WorkerMessage))
|
||||
PostMessage(WorkerMessage, func(msg WorkerMessage))
|
||||
Unwrap() WorkerInteractor
|
||||
}
|
||||
|
||||
var lastId int64 = 1 // access via atomic
|
||||
|
||||
type Backend interface {
|
||||
Run()
|
||||
Capabilities() *models.Capabilities
|
||||
PathSeparator() string
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
Backend Backend
|
||||
|
||||
actions chan WorkerMessage
|
||||
actionCallbacks map[int64]func(msg WorkerMessage)
|
||||
messageCallbacks map[int64]func(msg WorkerMessage)
|
||||
actionQueue *list.List
|
||||
status int32
|
||||
name string
|
||||
|
||||
sync.Mutex
|
||||
log.Logger
|
||||
}
|
||||
|
||||
func NewWorker(name string) *Worker {
|
||||
return &Worker{
|
||||
Logger: log.NewLogger(name, 2),
|
||||
actions: make(chan WorkerMessage),
|
||||
actionCallbacks: make(map[int64]func(msg WorkerMessage)),
|
||||
messageCallbacks: make(map[int64]func(msg WorkerMessage)),
|
||||
actionQueue: list.New(),
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (worker *Worker) Unwrap() WorkerInteractor {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (worker *Worker) Actions() chan WorkerMessage {
|
||||
return worker.actions
|
||||
}
|
||||
|
||||
func (worker *Worker) setId(msg WorkerMessage) {
|
||||
id := atomic.AddInt64(&lastId, 1)
|
||||
msg.setId(id)
|
||||
}
|
||||
|
||||
const (
|
||||
idle int32 = iota
|
||||
busy
|
||||
)
|
||||
|
||||
// Add a new task to the action queue without blocking. Start processing the
|
||||
// queue in the background if needed.
|
||||
func (worker *Worker) queue(msg WorkerMessage) {
|
||||
worker.Lock()
|
||||
defer worker.Unlock()
|
||||
worker.actionQueue.PushBack(msg)
|
||||
if atomic.LoadInt32(&worker.status) == idle {
|
||||
atomic.StoreInt32(&worker.status, busy)
|
||||
go worker.processQueue()
|
||||
}
|
||||
}
|
||||
|
||||
// Start processing the action queue and write all messages to the actions
|
||||
// channel, one by one. Stop when the action queue is empty.
|
||||
func (worker *Worker) processQueue() {
|
||||
defer log.PanicHandler()
|
||||
for {
|
||||
worker.Lock()
|
||||
e := worker.actionQueue.Front()
|
||||
if e == nil {
|
||||
atomic.StoreInt32(&worker.status, idle)
|
||||
worker.Unlock()
|
||||
return
|
||||
}
|
||||
msg := worker.actionQueue.Remove(e).(WorkerMessage)
|
||||
worker.Unlock()
|
||||
worker.actions <- msg
|
||||
}
|
||||
}
|
||||
|
||||
// PostAction posts an action to the worker. This method should not be called
|
||||
// from the same goroutine that the worker runs in or deadlocks may occur
|
||||
func (worker *Worker) PostAction(msg WorkerMessage, cb func(msg WorkerMessage)) {
|
||||
worker.setId(msg)
|
||||
// write to actions channel without blocking
|
||||
worker.queue(msg)
|
||||
|
||||
if cb != nil {
|
||||
worker.Lock()
|
||||
worker.actionCallbacks[msg.getId()] = cb
|
||||
worker.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var WorkerMessages = make(chan WorkerMessage, 50)
|
||||
|
||||
// PostMessage posts an message to the UI. This method should not be called
|
||||
// from the same goroutine that the UI runs in or deadlocks may occur
|
||||
func (worker *Worker) PostMessage(msg WorkerMessage,
|
||||
cb func(msg WorkerMessage),
|
||||
) {
|
||||
worker.setId(msg)
|
||||
msg.setAccount(worker.name)
|
||||
|
||||
WorkerMessages <- msg
|
||||
|
||||
if cb != nil {
|
||||
worker.Lock()
|
||||
worker.messageCallbacks[msg.getId()] = cb
|
||||
worker.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (worker *Worker) ProcessMessage(msg WorkerMessage) WorkerMessage {
|
||||
if inResponseTo := msg.InResponseTo(); inResponseTo != nil {
|
||||
worker.Lock()
|
||||
f, ok := worker.actionCallbacks[inResponseTo.getId()]
|
||||
worker.Unlock()
|
||||
if ok {
|
||||
f(msg)
|
||||
switch msg.(type) {
|
||||
case *Cancelled, *Done:
|
||||
worker.Lock()
|
||||
delete(worker.actionCallbacks, inResponseTo.getId())
|
||||
worker.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (worker *Worker) ProcessAction(msg WorkerMessage) WorkerMessage {
|
||||
if inResponseTo := msg.InResponseTo(); inResponseTo != nil {
|
||||
worker.Lock()
|
||||
f, ok := worker.messageCallbacks[inResponseTo.getId()]
|
||||
worker.Unlock()
|
||||
if ok {
|
||||
f(msg)
|
||||
if _, ok := msg.(*Done); ok {
|
||||
worker.Lock()
|
||||
delete(worker.messageCallbacks, inResponseTo.getId())
|
||||
worker.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (worker *Worker) PathSeparator() string {
|
||||
return worker.Backend.PathSeparator()
|
||||
}
|
||||
Reference in New Issue
Block a user