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 }