aerc/widgets/dirtree.go
Tim Culverhouse e1b62db583 fix: Set proper UIConfig for msgstores
The merged UIConfig used to create new message stores is based on the
selected directory. If the message store is created by other means than
selecting (ListDirectories for maildir/notmuch/mbox, or check-mail) it
may have an incorrect configuration if the user has folder-specific
values for:

- Threaded view
- Client built threads
- Client threads delay
- Sort criteria
- NewMessage bell

Use the correct merged UIConfig when creating a new message store.

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
Acked-by: Robin Jarry <robin@jarry.cc>
2022-08-22 09:30:37 +02:00

454 lines
9.8 KiB
Go

package widgets
import (
"fmt"
"sort"
"strconv"
"strings"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/worker/types"
"github.com/gdamore/tcell/v2"
)
type DirectoryTree struct {
*DirectoryList
listIdx int
list []*types.Thread
pathSeparator string
treeDirs []string
}
func NewDirectoryTree(dirlist *DirectoryList, pathSeparator string) DirectoryLister {
dt := &DirectoryTree{
DirectoryList: dirlist,
listIdx: -1,
list: make([]*types.Thread, 0),
pathSeparator: pathSeparator,
}
return dt
}
func (dt *DirectoryTree) ClearList() {
dt.list = make([]*types.Thread, 0)
}
func (dt *DirectoryTree) UpdateList(done func([]string)) {
dt.DirectoryList.UpdateList(func(dirs []string) {
if done != nil {
done(dirs)
}
dt.buildTree()
dt.listIdx = findString(dt.dirs, dt.selecting)
dt.Select(dt.selecting)
dt.Scrollable = Scrollable{}
})
}
func (dt *DirectoryTree) Draw(ctx *ui.Context) {
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
dt.UiConfig("").GetStyle(config.STYLE_DIRLIST_DEFAULT))
if dt.DirectoryList.spinner.IsRunning() {
dt.DirectoryList.spinner.Draw(ctx)
return
}
n := dt.countVisible(dt.list)
if n == 0 {
style := dt.UiConfig("").GetStyle(config.STYLE_DIRLIST_DEFAULT)
ctx.Printf(0, 0, style, dt.UiConfig("").EmptyDirlist)
return
}
dt.UpdateScroller(ctx.Height(), n)
dt.EnsureScroll(dt.countVisible(dt.list[:dt.listIdx]))
needScrollbar := true
percentVisible := float64(ctx.Height()) / float64(n)
if percentVisible >= 1.0 {
needScrollbar = false
}
textWidth := ctx.Width()
if needScrollbar {
textWidth -= 1
}
if textWidth < 0 {
textWidth = 0
}
rowNr := 0
for i, node := range dt.list {
if i < dt.Scroll() || !isVisible(node) {
continue
}
row := rowNr - dt.Scroll()
if row >= ctx.Height() {
break
}
name := dt.displayText(node)
rowNr++
dirStyle := []config.StyleObject{}
path := dt.getDirectory(node)
s := dt.getRUEString(path)
switch strings.Count(s, "/") {
case 1:
dirStyle = append(dirStyle, config.STYLE_DIRLIST_UNREAD)
case 2:
dirStyle = append(dirStyle, config.STYLE_DIRLIST_RECENT)
}
style := dt.UiConfig(path).GetComposedStyle(
config.STYLE_DIRLIST_DEFAULT, dirStyle)
if i == dt.listIdx {
style = dt.UiConfig(path).GetComposedStyleSelected(
config.STYLE_DIRLIST_DEFAULT, dirStyle)
}
ctx.Fill(0, row, textWidth, 1, ' ', style)
dirString := dt.getDirString(name, textWidth, func() string {
if path != "" {
return s
}
return ""
})
ctx.Printf(0, row, style, dirString)
}
if dt.NeedScrollbar() {
scrollBarCtx := ctx.Subcontext(ctx.Width()-1, 0, 1, ctx.Height())
dt.drawScrollbar(scrollBarCtx)
}
}
func (dt *DirectoryTree) MouseEvent(localX int, localY int, event tcell.Event) {
if event, ok := event.(*tcell.EventMouse); ok {
switch event.Buttons() {
case tcell.Button1:
clickedDir, ok := dt.Clicked(localX, localY)
if ok {
dt.Select(clickedDir)
}
case tcell.WheelDown:
dt.Next()
case tcell.WheelUp:
dt.Prev()
}
}
}
func (dt *DirectoryTree) Clicked(x int, y int) (string, bool) {
if dt.list == nil || len(dt.list) == 0 || dt.countVisible(dt.list) < y {
return "", false
}
for i, node := range dt.list {
if dt.countVisible(dt.list[:i]) == y {
if path := dt.getDirectory(node); path != "" {
return path, true
}
}
}
return "", false
}
func (dt *DirectoryTree) Select(name string) {
idx := findString(dt.treeDirs, name)
if idx >= 0 {
selIdx, node := dt.getTreeNode(uint32(idx))
if node != nil {
makeVisible(node)
dt.listIdx = selIdx
}
}
if name == "" {
return
}
dt.DirectoryList.Select(name)
}
func (dt *DirectoryTree) NextPrev(delta int) {
newIdx := dt.listIdx
ndirs := len(dt.list)
if newIdx == ndirs {
return
}
if ndirs == 0 {
return
}
step := 1
if delta < 0 {
step = -1
delta *= -1
}
for i := 0; i < delta; {
newIdx += step
if newIdx < 0 {
newIdx = ndirs - 1
} else if newIdx >= ndirs {
newIdx = 0
}
if isVisible(dt.list[newIdx]) {
i++
}
}
dt.listIdx = newIdx
if path := dt.getDirectory(dt.list[dt.listIdx]); path != "" {
dt.Select(path)
}
}
func (dt *DirectoryTree) CollapseFolder() {
if dt.listIdx >= 0 && dt.listIdx < len(dt.list) {
if node := dt.list[dt.listIdx]; node != nil {
if node.Parent != nil && (node.Hidden || node.FirstChild == nil) {
node.Parent.Hidden = true
// highlight parent node and select it
for i, t := range dt.list {
if t == node.Parent {
dt.listIdx = i
if path := dt.getDirectory(dt.list[dt.listIdx]); path != "" {
dt.Select(path)
}
}
}
} else {
node.Hidden = true
}
dt.Invalidate()
}
}
}
func (dt *DirectoryTree) ExpandFolder() {
if dt.listIdx >= 0 && dt.listIdx < len(dt.list) {
dt.list[dt.listIdx].Hidden = false
dt.Invalidate()
}
}
func (dt *DirectoryTree) countVisible(list []*types.Thread) (n int) {
for _, node := range list {
if isVisible(node) {
n++
}
}
return
}
func (dt *DirectoryTree) displayText(node *types.Thread) string {
elems := strings.Split(dt.treeDirs[getAnyUid(node)], dt.pathSeparator)
return fmt.Sprintf("%s%s%s", threadPrefix(node), getFlag(node), elems[countLevels(node)])
}
func (dt *DirectoryTree) getDirectory(node *types.Thread) string {
if uid := node.Uid; int(uid) < len(dt.treeDirs) {
return dt.treeDirs[uid]
}
return ""
}
func (dt *DirectoryTree) getTreeNode(uid uint32) (int, *types.Thread) {
var found *types.Thread
var idx int
for i, node := range dt.list {
if node.Uid == uid {
found = node
idx = i
}
}
return idx, found
}
func (dt *DirectoryTree) hiddenDirectories() map[string]bool {
hidden := make(map[string]bool, 0)
for _, node := range dt.list {
if node.Hidden && node.FirstChild != nil {
elems := strings.Split(dt.treeDirs[getAnyUid(node)], dt.pathSeparator)
if levels := countLevels(node); levels < len(elems) {
if node.FirstChild != nil && (levels+1) < len(elems) {
levels += 1
}
if dirStr := strings.Join(elems[:levels], dt.pathSeparator); dirStr != "" {
hidden[dirStr] = true
}
}
}
}
return hidden
}
func (dt *DirectoryTree) setHiddenDirectories(hiddenDirs map[string]bool) {
for _, node := range dt.list {
elems := strings.Split(dt.treeDirs[getAnyUid(node)], dt.pathSeparator)
if levels := countLevels(node); levels < len(elems) {
if node.FirstChild != nil && (levels+1) < len(elems) {
levels += 1
}
strDir := strings.Join(elems[:levels], dt.pathSeparator)
if hidden, ok := hiddenDirs[strDir]; hidden && ok {
node.Hidden = true
}
}
}
}
func (dt *DirectoryTree) buildTree() {
if len(dt.list) != 0 {
hiddenDirs := dt.hiddenDirectories()
defer func() {
dt.setHiddenDirectories(hiddenDirs)
}()
}
sTree := make([][]string, 0)
for i, dir := range dt.dirs {
elems := strings.Split(dir, dt.pathSeparator)
if len(elems) == 0 {
continue
}
elems = append(elems, fmt.Sprintf("%d", i))
sTree = append(sTree, elems)
}
dt.treeDirs = make([]string, len(dt.dirs))
copy(dt.treeDirs, dt.dirs)
root := &types.Thread{Uid: 0}
buildTree(root, sTree, 0xFFFFFF)
threads := make([]*types.Thread, 0)
for iter := root.FirstChild; iter != nil; iter = iter.NextSibling {
iter.Parent = nil
threads = append(threads, iter)
}
// folders-sort
if dt.DirectoryList.acctConf.EnableFoldersSort {
toStr := func(t *types.Thread) string {
if elems := strings.Split(dt.treeDirs[getAnyUid(t)], dt.pathSeparator); len(elems) > 0 {
return elems[0]
}
return ""
}
sort.Slice(threads, func(i, j int) bool {
foldersSort := dt.DirectoryList.acctConf.FoldersSort
iInFoldersSort := findString(foldersSort, toStr(threads[i]))
jInFoldersSort := findString(foldersSort, toStr(threads[j]))
if iInFoldersSort >= 0 && jInFoldersSort >= 0 {
return iInFoldersSort < jInFoldersSort
}
if iInFoldersSort >= 0 {
return true
}
if jInFoldersSort >= 0 {
return false
}
return toStr(threads[i]) < toStr(threads[j])
})
}
dt.list = make([]*types.Thread, 0)
for _, node := range threads {
err := node.Walk(func(t *types.Thread, lvl int, err error) error {
dt.list = append(dt.list, t)
return nil
})
if err != nil {
logging.Warnf("failed to walk tree: %v", err)
}
}
}
func buildTree(node *types.Thread, stree [][]string, defaultUid uint32) {
m := make(map[string][][]string)
for _, branch := range stree {
if len(branch) > 1 {
next := append(m[branch[0]], branch[1:]) //nolint:gocritic // intentional append to different slice
m[branch[0]] = next
}
}
keys := make([]string, 0)
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
next := m[key]
var uid uint32 = defaultUid
for _, testStr := range next {
if len(testStr) == 1 {
if uidI, err := strconv.Atoi(next[0][0]); err == nil {
uid = uint32(uidI)
}
}
}
nextNode := &types.Thread{Uid: uid}
node.AddChild(nextNode)
buildTree(nextNode, next, defaultUid)
}
}
func makeVisible(node *types.Thread) {
if node == nil {
return
}
for iter := node.Parent; iter != nil; iter = iter.Parent {
iter.Hidden = false
}
}
func isVisible(node *types.Thread) bool {
isVisible := true
for iter := node.Parent; iter != nil; iter = iter.Parent {
if iter.Hidden {
isVisible = false
break
}
}
return isVisible
}
func getAnyUid(node *types.Thread) (uid uint32) {
err := node.Walk(func(t *types.Thread, l int, err error) error {
if t.FirstChild == nil {
uid = t.Uid
}
return nil
})
if err != nil {
logging.Warnf("failed to get uid: %v", err)
}
return
}
func countLevels(node *types.Thread) (level int) {
for iter := node.Parent; iter != nil; iter = iter.Parent {
level++
}
return
}
func getFlag(node *types.Thread) (flag string) {
if node != nil && node.FirstChild != nil {
if node.Hidden {
flag = "─"
} else {
flag = "┌"
}
}
return
}