aerc/widgets/dirlist.go

405 lines
9.2 KiB
Go
Raw Normal View History

2019-01-13 20:25:46 +01:00
package widgets
import (
"fmt"
2019-01-13 20:25:46 +01:00
"log"
"regexp"
2019-01-13 20:25:46 +01:00
"sort"
"github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
2019-01-13 20:25:46 +01:00
2019-05-18 02:57:10 +02:00
"git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib"
2020-02-19 17:09:26 +01:00
libsort "git.sr.ht/~sircmpwn/aerc/lib/sort"
2019-05-18 02:57:10 +02:00
"git.sr.ht/~sircmpwn/aerc/lib/ui"
"git.sr.ht/~sircmpwn/aerc/models"
2019-05-18 02:57:10 +02:00
"git.sr.ht/~sircmpwn/aerc/worker/types"
2019-01-13 20:25:46 +01:00
)
type DirectoryList struct {
ui.Invalidatable
2020-01-26 12:43:46 +01:00
aercConf *config.AercConfig
acctConf *config.AccountConfig
store *lib.DirStore
dirs []string
logger *log.Logger
selecting string
selected string
spinner *Spinner
worker *types.Worker
2019-01-13 20:25:46 +01:00
}
2020-01-26 12:43:46 +01:00
func NewDirectoryList(conf *config.AercConfig, acctConf *config.AccountConfig,
logger *log.Logger, worker *types.Worker) *DirectoryList {
2019-01-14 02:02:21 +01:00
dirlist := &DirectoryList{
2020-01-26 12:43:46 +01:00
aercConf: conf,
acctConf: acctConf,
logger: logger,
store: lib.NewDirStore(),
worker: worker,
2019-01-14 02:02:21 +01:00
}
2020-01-26 12:43:46 +01:00
uiConf := dirlist.UiConfig()
dirlist.spinner = NewSpinner(&uiConf)
2019-01-14 02:02:21 +01:00
dirlist.spinner.OnInvalidate(func(_ ui.Drawable) {
dirlist.Invalidate()
})
dirlist.spinner.Start()
return dirlist
2019-01-13 20:25:46 +01:00
}
2020-01-26 12:43:46 +01:00
func (dirlist *DirectoryList) UiConfig() config.UIConfig {
return dirlist.aercConf.GetUiConfig(map[config.ContextType]string{
config.UI_CONTEXT_ACCOUNT: dirlist.acctConf.Name,
config.UI_CONTEXT_FOLDER: dirlist.Selected(),
})
}
func (dirlist *DirectoryList) List() []string {
return dirlist.store.List()
}
func (dirlist *DirectoryList) UpdateList(done func(dirs []string)) {
2019-07-04 18:31:27 +02:00
// TODO: move this logic into dirstore
var dirs []string
2019-01-13 20:25:46 +01:00
dirlist.worker.PostAction(
&types.ListDirectories{}, func(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.Directory:
dirs = append(dirs, msg.Dir.Name)
2019-01-13 20:25:46 +01:00
case *types.Done:
dirlist.store.Update(dirs)
dirlist.filterDirsByFoldersConfig()
dirlist.sortDirsByFoldersSortConfig()
dirlist.store.Update(dirlist.dirs)
2019-01-14 02:02:21 +01:00
dirlist.spinner.Stop()
dirlist.Invalidate()
if done != nil {
done(dirs)
}
2019-01-13 20:25:46 +01:00
}
})
}
func (dirlist *DirectoryList) Select(name string) {
2019-03-15 03:34:34 +01:00
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:
2019-03-15 03:34:34 +01:00
dirlist.selected = dirlist.selecting
2019-06-12 08:31:51 +02:00
dirlist.filterDirsByFoldersConfig()
hasSelected := false
for _, d := range dirlist.dirs {
2019-06-12 08:31:51 +02:00
if d == dirlist.selected {
hasSelected = true
break
}
}
if !hasSelected && dirlist.selected != "" {
dirlist.dirs = append(dirlist.dirs, dirlist.selected)
2019-06-12 08:31:51 +02:00
}
sort.Strings(dirlist.dirs)
dirlist.sortDirsByFoldersSortConfig()
2019-03-15 03:34:34 +01:00
}
dirlist.Invalidate()
})
dirlist.Invalidate()
}
2019-03-15 03:34:34 +01:00
func (dirlist *DirectoryList) Selected() string {
return dirlist.selected
}
2019-01-13 20:25:46 +01:00
func (dirlist *DirectoryList) Invalidate() {
dirlist.DoInvalidate(dirlist)
2019-01-13 20:25:46 +01:00
}
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), "…")
}
2020-01-26 12:43:46 +01:00
for _, char := range dirlist.UiConfig().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 {
msgStore, ok := dirlist.MsgStore(name)
if !ok {
return ""
}
var totalRecent, totalUnseen, totalExists int
if msgStore.DirInfo.AccurateCounts {
totalRecent = msgStore.DirInfo.Recent
totalUnseen = msgStore.DirInfo.Unseen
totalExists = msgStore.DirInfo.Exists
} else {
totalRecent, totalUnseen, totalExists = countRUE(msgStore)
}
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
}
2019-01-13 20:25:46 +01:00
func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
2019-01-14 02:02:21 +01:00
if dirlist.spinner.IsRunning() {
dirlist.spinner.Draw(ctx)
return
}
if len(dirlist.dirs) == 0 {
style := tcell.StyleDefault
2020-01-26 12:43:46 +01:00
ctx.Printf(0, 0, style, dirlist.UiConfig().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++
}
2019-01-13 20:25:46 +01:00
}
2019-03-11 02:15:24 +01:00
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)
Ensure we aren't selecting negative directories When the list of directories is empty trying to navigate in the directory list did previously lead to a crash. With this change we instead return early before trying to change the directory. Example backtrace: > panic: runtime error: index out of range [-1] > > goroutine 1 [running]: > git.sr.ht/~sircmpwn/aerc/widgets.(*DirectoryList).NextPrev(0xc000160680, 0xffffffffffffffff) > source/aerc/widgets/dirlist.go:285 +0xd4 > git.sr.ht/~sircmpwn/aerc/commands/account.NextPrevFolder.Execute(0xc000191040, 0xc00025c210, 0x1, 0x1, 0x0, 0xc00016f420) > source/aerc/commands/account/next-folder.go:44 +0xe0 > git.sr.ht/~sircmpwn/aerc/commands.(*Commands).ExecuteCommand(0xc0000101a8, 0xc000191040, 0xc00025c210, 0x1, 0x1, 0xc000020070, 0xb46d01) > source/aerc/commands/commands.go:66 +0xa7 > main.execCommand(0xc000191040, 0xc0001ca190, 0xc00025c210, 0x1, 0x1, 0xc00025c210, 0xc0003fb080) > source/aerc/aerc.go:60 +0xc7 > main.main.func3(0xc00025c210, 0x1, 0x1, 0x1, 0x1) > source/aerc/aerc.go:162 +0x57 > git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).BeginExCommand.func1(0xc000201db0, 0xb) > source/aerc/widgets/aerc.go:382 +0x83 > git.sr.ht/~sircmpwn/aerc/widgets.(*ExLine).Event(0xc0003be100, 0xb475a0, 0xc00023cba0, 0xc00023cba0) > source/aerc/widgets/exline.go:79 +0x131 > git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).Event(0xc000191040, 0xb475a0, 0xc00023cba0, 0x99ee01) > source/aerc/widgets/aerc.go:202 +0x4c1 > git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).simulate(0xc000191040, 0xc000036f00, 0xd, 0x10) > source/aerc/widgets/aerc.go:195 +0x8d > git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).Event(0xc000191040, 0xb475a0, 0xc00023c9c0, 0x9c5a60) > source/aerc/widgets/aerc.go:218 +0x3e8 > git.sr.ht/~sircmpwn/aerc/lib/ui.(*UI).Tick(0xc0001ca190, 0xa99d00) > source/aerc/lib/ui/ui.go:92 +0x190 > main.main() > source/aerc/aerc.go:192 +0x5f2
2020-02-06 12:47:38 +01:00
if ndirs == 0 {
return
}
if newIdx < 0 {
newIdx = ndirs - 1
} else if newIdx >= ndirs {
newIdx = 0
2019-03-11 02:15:24 +01:00
}
Ensure we aren't selecting negative directories When the list of directories is empty trying to navigate in the directory list did previously lead to a crash. With this change we instead return early before trying to change the directory. Example backtrace: > panic: runtime error: index out of range [-1] > > goroutine 1 [running]: > git.sr.ht/~sircmpwn/aerc/widgets.(*DirectoryList).NextPrev(0xc000160680, 0xffffffffffffffff) > source/aerc/widgets/dirlist.go:285 +0xd4 > git.sr.ht/~sircmpwn/aerc/commands/account.NextPrevFolder.Execute(0xc000191040, 0xc00025c210, 0x1, 0x1, 0x0, 0xc00016f420) > source/aerc/commands/account/next-folder.go:44 +0xe0 > git.sr.ht/~sircmpwn/aerc/commands.(*Commands).ExecuteCommand(0xc0000101a8, 0xc000191040, 0xc00025c210, 0x1, 0x1, 0xc000020070, 0xb46d01) > source/aerc/commands/commands.go:66 +0xa7 > main.execCommand(0xc000191040, 0xc0001ca190, 0xc00025c210, 0x1, 0x1, 0xc00025c210, 0xc0003fb080) > source/aerc/aerc.go:60 +0xc7 > main.main.func3(0xc00025c210, 0x1, 0x1, 0x1, 0x1) > source/aerc/aerc.go:162 +0x57 > git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).BeginExCommand.func1(0xc000201db0, 0xb) > source/aerc/widgets/aerc.go:382 +0x83 > git.sr.ht/~sircmpwn/aerc/widgets.(*ExLine).Event(0xc0003be100, 0xb475a0, 0xc00023cba0, 0xc00023cba0) > source/aerc/widgets/exline.go:79 +0x131 > git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).Event(0xc000191040, 0xb475a0, 0xc00023cba0, 0x99ee01) > source/aerc/widgets/aerc.go:202 +0x4c1 > git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).simulate(0xc000191040, 0xc000036f00, 0xd, 0x10) > source/aerc/widgets/aerc.go:195 +0x8d > git.sr.ht/~sircmpwn/aerc/widgets.(*Aerc).Event(0xc000191040, 0xb475a0, 0xc00023c9c0, 0x9c5a60) > source/aerc/widgets/aerc.go:218 +0x3e8 > git.sr.ht/~sircmpwn/aerc/lib/ui.(*UI).Tick(0xc0001ca190, 0xa99d00) > source/aerc/lib/ui/ui.go:92 +0x190 > main.main() > source/aerc/aerc.go:192 +0x5f2
2020-02-06 12:47:38 +01:00
dirlist.Select(dirlist.dirs[newIdx])
2019-03-11 02:15:24 +01:00
}
func (dirlist *DirectoryList) Next() {
dirlist.NextPrev(1)
2019-03-11 02:15:24 +01:00
}
func (dirlist *DirectoryList) Prev() {
dirlist.NextPrev(-1)
2019-03-11 02:15:24 +01:00
}
2019-06-12 08:31:51 +02:00
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 {
2020-01-26 12:43:46 +01:00
foldersSort := dirlist.acctConf.FoldersSort
iInFoldersSort := findString(foldersSort, dirlist.dirs[i])
jInFoldersSort := findString(foldersSort, dirlist.dirs[j])
if iInFoldersSort >= 0 && jInFoldersSort >= 0 {
return iInFoldersSort < jInFoldersSort
}
if iInFoldersSort >= 0 {
return true
}
if jInFoldersSort >= 0 {
return false
}
return dirlist.dirs[i] < dirlist.dirs[j]
})
}
// filterDirsByFoldersConfig sets dirlist.dirs to the filtered subset of the
// dirstore, based on the AccountConfig.Folders option
2019-06-12 08:31:51 +02:00
func (dirlist *DirectoryList) filterDirsByFoldersConfig() {
dirlist.dirs = dirlist.store.List()
2019-06-12 08:31:51 +02:00
// config option defaults to show all if unset
2020-01-26 12:43:46 +01:00
configFolders := dirlist.acctConf.Folders
if len(configFolders) == 0 {
2019-06-12 08:31:51 +02:00
return
}
var filtered []string
for _, folder := range dirlist.dirs {
2020-01-26 12:43:46 +01:00
for _, cfgfolder := range configFolders {
if folderMatches(folder, cfgfolder) {
2019-06-12 08:31:51 +02:00
filtered = append(filtered, folder)
break
}
}
}
dirlist.dirs = filtered
2019-06-12 08:31:51 +02:00
}
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
}
func (dirlist *DirectoryList) getSortCriteria() []*types.SortCriterion {
if len(dirlist.UiConfig().Sort) == 0 {
return nil
}
criteria, err := libsort.GetSortCriteria(dirlist.UiConfig().Sort)
if err != nil {
dirlist.logger.Printf("getSortCriteria failed: %v", err)
return nil
}
return criteria
}
func countRUE(msgStore *lib.MessageStore) (recent, unread, exist int) {
for _, msg := range msgStore.Messages {
if msg == nil {
continue
}
seen := false
isrecent := false
for _, flag := range msg.Flags {
if flag == models.SeenFlag {
seen = true
} else if flag == models.RecentFlag {
isrecent = true
}
}
if !seen {
if isrecent {
recent++
} else {
unread++
}
}
exist++
}
return recent, unread, exist
}