aerc/widgets/dirlist.go
Michele Finotto dfe58842b9 Add custom sorting for folders
A new config options for accounts.conf (folders-sort) was added to
allow a user to choose which folders should be shown on top.
My use case was to avoid stepping into heavy, but rarely viewed folders
when cycling through other often accessed ones.

To test add this to your account.conf:

folders-sort  = INBOX,Sent,Archive

INBOX, Sent and Archive should then show at the top of your dirlist,
and all other folders should come next in alphabetical order.
2019-12-09 12:42:40 -05:00

366 lines
8.3 KiB
Go

package widgets
import (
"fmt"
"log"
"regexp"
"sort"
"strings"
"github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
"git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib"
"git.sr.ht/~sircmpwn/aerc/lib/ui"
"git.sr.ht/~sircmpwn/aerc/models"
"git.sr.ht/~sircmpwn/aerc/worker/types"
)
type DirectoryList struct {
ui.Invalidatable
acctConf *config.AccountConfig
uiConf *config.UIConfig
store *lib.DirStore
dirs []string
logger *log.Logger
selecting string
selected string
spinner *Spinner
worker *types.Worker
}
func NewDirectoryList(acctConf *config.AccountConfig, uiConf *config.UIConfig,
logger *log.Logger, worker *types.Worker) *DirectoryList {
dirlist := &DirectoryList{
acctConf: acctConf,
uiConf: uiConf,
logger: logger,
spinner: NewSpinner(uiConf),
store: lib.NewDirStore(),
worker: worker,
}
dirlist.spinner.OnInvalidate(func(_ ui.Drawable) {
dirlist.Invalidate()
})
dirlist.spinner.Start()
return dirlist
}
func (dirlist *DirectoryList) List() []string {
return dirlist.store.List()
}
func (dirlist *DirectoryList) UpdateList(done func(dirs []string)) {
// TODO: move this logic into dirstore
var dirs []string
dirlist.worker.PostAction(
&types.ListDirectories{}, func(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.Directory:
dirs = append(dirs, msg.Dir.Name)
case *types.Done:
dirlist.store.Update(dirs)
dirlist.filterDirsByFoldersConfig()
dirlist.sortDirsByFoldersSortConfig()
dirlist.store.Update(dirlist.dirs)
dirlist.spinner.Stop()
dirlist.Invalidate()
if done != nil {
done(dirs)
}
}
})
}
func (dirlist *DirectoryList) Select(name string) {
dirlist.selecting = name
dirlist.worker.PostAction(&types.OpenDirectory{Directory: name},
func(msg types.WorkerMessage) {
switch msg.(type) {
case *types.Error:
dirlist.selecting = ""
case *types.Done:
dirlist.selected = dirlist.selecting
dirlist.filterDirsByFoldersConfig()
hasSelected := false
for _, d := range dirlist.dirs {
if d == dirlist.selected {
hasSelected = true
break
}
}
if !hasSelected && dirlist.selected != "" {
dirlist.dirs = append(dirlist.dirs, dirlist.selected)
}
sort.Strings(dirlist.dirs)
dirlist.sortDirsByFoldersSortConfig()
}
dirlist.Invalidate()
})
dirlist.Invalidate()
}
func (dirlist *DirectoryList) Selected() string {
return dirlist.selected
}
func (dirlist *DirectoryList) Invalidate() {
dirlist.DoInvalidate(dirlist)
}
func (dirlist *DirectoryList) getDirString(name string, width int, recentUnseen func() string) string {
percent := false
rightJustify := false
formatted := ""
doRightJustify := func(s string) {
formatted = runewidth.FillRight(formatted, width-len(s))
formatted = runewidth.Truncate(formatted, width-len(s), "…")
}
for _, char := range dirlist.uiConf.DirListFormat {
switch char {
case '%':
if percent {
formatted += string(char)
percent = false
} else {
percent = true
}
case '>':
if percent {
rightJustify = true
}
case 'n':
if percent {
if rightJustify {
doRightJustify(name)
rightJustify = false
}
formatted += name
percent = false
}
case 'r':
if percent {
rString := recentUnseen()
if rightJustify {
doRightJustify(rString)
rightJustify = false
}
formatted += rString
percent = false
}
default:
formatted += string(char)
}
}
return formatted
}
func (dirlist *DirectoryList) getRUEString(name string) string {
totalUnseen := 0
totalRecent := 0
totalExists := 0
if msgStore, ok := dirlist.MsgStore(name); ok {
for _, msg := range msgStore.Messages {
if msg == nil {
continue
}
seen := false
recent := false
for _, flag := range msg.Flags {
if flag == models.SeenFlag {
seen = true
} else if flag == models.RecentFlag {
recent = true
}
}
if !seen {
if recent {
totalRecent++
} else {
totalUnseen++
}
}
}
totalExists = msgStore.DirInfo.Exists
}
rueString := ""
if totalRecent > 0 {
rueString = fmt.Sprintf("%d/%d/%d", totalRecent, totalUnseen, totalExists)
} else if totalUnseen > 0 {
rueString = fmt.Sprintf("%d/%d", totalUnseen, totalExists)
} else if totalExists > 0 {
rueString = fmt.Sprintf("%d", totalExists)
}
return rueString
}
func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
if dirlist.spinner.IsRunning() {
dirlist.spinner.Draw(ctx)
return
}
if len(dirlist.dirs) == 0 {
style := tcell.StyleDefault
ctx.Printf(0, 0, style, dirlist.uiConf.EmptyDirlist)
return
}
row := 0
for _, name := range dirlist.dirs {
if row >= ctx.Height() {
break
}
style := tcell.StyleDefault
if name == dirlist.selected {
style = style.Reverse(true)
} else if name == dirlist.selecting {
style = style.Reverse(true)
style = style.Foreground(tcell.ColorGray)
}
ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
dirString := dirlist.getDirString(name, ctx.Width(), func() string {
return dirlist.getRUEString(name)
})
ctx.Printf(0, row, style, dirString)
row++
}
}
func (dirlist *DirectoryList) MouseEvent(localX int, localY int, event tcell.Event) {
switch event := event.(type) {
case *tcell.EventMouse:
switch event.Buttons() {
case tcell.Button1:
clickedDir, ok := dirlist.Clicked(localX, localY)
if ok {
dirlist.Select(clickedDir)
}
case tcell.WheelDown:
dirlist.Next()
case tcell.WheelUp:
dirlist.Prev()
}
}
}
func (dirlist *DirectoryList) Clicked(x int, y int) (string, bool) {
if dirlist.dirs == nil || len(dirlist.dirs) == 0 {
return "", false
}
for i, name := range dirlist.dirs {
if i == y {
return name, true
}
}
return "", false
}
func (dirlist *DirectoryList) NextPrev(delta int) {
curIdx := findString(dirlist.dirs, dirlist.selected)
if curIdx == len(dirlist.dirs) {
return
}
newIdx := curIdx + delta
ndirs := len(dirlist.dirs)
if newIdx < 0 {
newIdx = ndirs - 1
} else if newIdx >= ndirs {
newIdx = 0
}
dirlist.Select(dirlist.dirs[newIdx])
}
func (dirlist *DirectoryList) Next() {
dirlist.NextPrev(1)
}
func (dirlist *DirectoryList) Prev() {
dirlist.NextPrev(-1)
}
func folderMatches(folder string, pattern string) bool {
if len(pattern) == 0 {
return false
}
if pattern[0] == '~' {
r, err := regexp.Compile(pattern[1:])
if err != nil {
return false
}
return r.Match([]byte(folder))
}
return pattern == folder
}
// sortDirsByFoldersSortConfig sets dirlist.dirs to be sorted based on the
// AccountConfig.FoldersSort option. Folders not included in the option
// will be appended at the end in alphabetical order
func (dirlist *DirectoryList) sortDirsByFoldersSortConfig() {
sort.Slice(dirlist.dirs, func(i, j int) bool {
iInFoldersSort := findString(dirlist.acctConf.FoldersSort, dirlist.dirs[i])
jInFoldersSort := findString(dirlist.acctConf.FoldersSort, dirlist.dirs[j])
if iInFoldersSort >= 0 && jInFoldersSort >= 0 {
return iInFoldersSort < jInFoldersSort
}
if iInFoldersSort >= 0 {
return true
}
if jInFoldersSort >= 0 {
return false
}
return strings.Compare(dirlist.dirs[i], dirlist.dirs[j]) == -1
})
}
// filterDirsByFoldersConfig sets dirlist.dirs to the filtered subset of the
// dirstore, based on the AccountConfig.Folders option
func (dirlist *DirectoryList) filterDirsByFoldersConfig() {
dirlist.dirs = dirlist.store.List()
// config option defaults to show all if unset
if len(dirlist.acctConf.Folders) == 0 {
return
}
var filtered []string
for _, folder := range dirlist.dirs {
for _, cfgfolder := range dirlist.acctConf.Folders {
if folderMatches(folder, cfgfolder) {
filtered = append(filtered, folder)
break
}
}
}
dirlist.dirs = filtered
}
func (dirlist *DirectoryList) SelectedMsgStore() (*lib.MessageStore, bool) {
return dirlist.store.MessageStore(dirlist.selected)
}
func (dirlist *DirectoryList) MsgStore(name string) (*lib.MessageStore, bool) {
return dirlist.store.MessageStore(name)
}
func (dirlist *DirectoryList) SetMsgStore(name string, msgStore *lib.MessageStore) {
dirlist.store.SetMessageStore(name, msgStore)
msgStore.OnUpdateDirs(func() {
dirlist.Invalidate()
})
}
func findString(slice []string, str string) int {
for i, s := range slice {
if str == s {
return i
}
}
return -1
}