dfe58842b9
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.
365 lines
8.3 KiB
Go
365 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
|
|
}
|