mirror of
https://git.sr.ht/~rjarry/aerc
synced 2025-02-22 23:23:57 +01:00

Use go-opt v2 new completion API which returns items descriptions along with their text values. Display the descriptions after the items separated by two spaces. Wrap the descriptions in parentheses to better indicate that they are not part of the completion choices. Limit the description length to 80 characters to avoid display issues. Add a new style object completion_description in stylesets. By default, the object will be rendered with a dimmed terminal attribute. Update all stylesets and documentation accordingly. Implements: https://todo.sr.ht/~rjarry/aerc/271 Link: https://git.sr.ht/~rjarry/go-opt/commit/ebeb82538395a Changelog-added: Command completion now displays descriptions next to completion items. Changelog-added: New `completion_description` style object in style sets used for rendering completion item descriptions. Signed-off-by: Robin Jarry <robin@jarry.cc> Tested-by: Bojan Gabric <bojan@bojangabric.com> Tested-by: Jason Cox <me@jasoncarloscox.com> Acked-by: Tim Culverhouse <tim@timculverhouse.com>
201 lines
4.2 KiB
Go
201 lines
4.2 KiB
Go
package lib
|
|
|
|
import (
|
|
"io"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"git.sr.ht/~rjarry/aerc/lib"
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
"git.sr.ht/~rjarry/aerc/lib/rfc822"
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
"git.sr.ht/~rjarry/aerc/worker/types"
|
|
"git.sr.ht/~rjarry/go-opt/v2"
|
|
)
|
|
|
|
func Search(messages []rfc822.RawMessage, criteria *types.SearchCriteria) ([]models.UID, error) {
|
|
criteria.PrepareHeader()
|
|
requiredParts := GetRequiredParts(criteria)
|
|
|
|
var matchedUids []models.UID
|
|
for _, m := range messages {
|
|
success, err := SearchMessage(m, criteria, requiredParts)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if success {
|
|
matchedUids = append(matchedUids, m.UID())
|
|
}
|
|
}
|
|
|
|
return matchedUids, nil
|
|
}
|
|
|
|
// searchMessage executes the search criteria for the given RawMessage,
|
|
// returns true if search succeeded
|
|
func SearchMessage(message rfc822.RawMessage, criteria *types.SearchCriteria,
|
|
parts MsgParts,
|
|
) (bool, error) {
|
|
if criteria == nil {
|
|
return true, nil
|
|
}
|
|
// setup parts of the message to use in the search
|
|
// this is so that we try to minimise reading unnecessary parts
|
|
var (
|
|
flags models.Flags
|
|
info *models.MessageInfo
|
|
text string
|
|
err error
|
|
)
|
|
|
|
if parts&FLAGS > 0 {
|
|
flags, err = message.ModelFlags()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
info, err = rfc822.MessageInfo(message)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
switch {
|
|
case parts&BODY > 0:
|
|
path := lib.FindFirstNonMultipart(info.BodyStructure, nil)
|
|
reader, err := message.NewReader()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer reader.Close()
|
|
msg, err := rfc822.ReadMessage(reader)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
part, err := rfc822.FetchEntityPartReader(msg, path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
bytes, err := io.ReadAll(part)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
text = string(bytes)
|
|
case parts&ALL > 0:
|
|
reader, err := message.NewReader()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer reader.Close()
|
|
bytes, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
text = string(bytes)
|
|
default:
|
|
text = info.Envelope.Subject
|
|
}
|
|
|
|
// now search through the criteria
|
|
// implicit AND at the moment so fail fast
|
|
if criteria.Headers != nil {
|
|
for k, v := range criteria.Headers {
|
|
headerValue := info.RFC822Headers.Get(k)
|
|
for _, text := range v {
|
|
if !containsSmartCase(headerValue, text) {
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
args := opt.LexArgs(strings.Join(criteria.Terms, " "))
|
|
for _, searchTerm := range args.Args() {
|
|
if !containsSmartCase(text, searchTerm) {
|
|
return false, nil
|
|
}
|
|
}
|
|
if criteria.WithFlags != 0 {
|
|
if !flags.Has(criteria.WithFlags) {
|
|
return false, nil
|
|
}
|
|
}
|
|
if criteria.WithoutFlags != 0 {
|
|
if flags.Has(criteria.WithoutFlags) {
|
|
return false, nil
|
|
}
|
|
}
|
|
if parts&DATE > 0 {
|
|
if date, err := info.RFC822Headers.Date(); err != nil {
|
|
log.Errorf("Failed to get date from header: %v", err)
|
|
} else {
|
|
if !criteria.StartDate.IsZero() {
|
|
if date.Before(criteria.StartDate) {
|
|
return false, nil
|
|
}
|
|
}
|
|
if !criteria.EndDate.IsZero() {
|
|
if date.After(criteria.EndDate) {
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// containsSmartCase is a smarter version of strings.Contains for searching.
|
|
// Is case-insensitive unless substr contains an upper case character
|
|
func containsSmartCase(s string, substr string) bool {
|
|
if hasUpper(substr) {
|
|
return strings.Contains(s, substr)
|
|
}
|
|
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
|
}
|
|
|
|
func hasUpper(s string) bool {
|
|
for _, r := range s {
|
|
if unicode.IsUpper(r) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// The parts of a message, kind of
|
|
type MsgParts int
|
|
|
|
const NONE MsgParts = 0
|
|
const (
|
|
FLAGS MsgParts = 1 << iota
|
|
HEADER
|
|
DATE
|
|
BODY
|
|
ALL
|
|
)
|
|
|
|
// Returns a bitmask of the parts of the message required to be loaded for the
|
|
// given criteria
|
|
func GetRequiredParts(criteria *types.SearchCriteria) MsgParts {
|
|
required := NONE
|
|
if criteria == nil {
|
|
return required
|
|
}
|
|
if len(criteria.Headers) > 0 {
|
|
required |= HEADER
|
|
}
|
|
if !criteria.StartDate.IsZero() || !criteria.EndDate.IsZero() {
|
|
required |= DATE
|
|
}
|
|
if criteria.SearchBody {
|
|
required |= BODY
|
|
}
|
|
if criteria.SearchAll {
|
|
required |= ALL
|
|
}
|
|
if criteria.WithFlags != 0 {
|
|
required |= FLAGS
|
|
}
|
|
if criteria.WithoutFlags != 0 {
|
|
required |= FLAGS
|
|
}
|
|
|
|
return required
|
|
}
|