package widgets import ( "fmt" "sort" "strconv" "strings" "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/lib/state" "git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/worker/types" "github.com/gdamore/tcell/v2" ) type DirectoryTree struct { *DirectoryList listIdx int list []*types.Thread treeDirs []string virtual bool virtualCb func() } func NewDirectoryTree(dirlist *DirectoryList) DirectoryLister { dt := &DirectoryTree{ DirectoryList: dirlist, listIdx: -1, list: make([]*types.Thread, 0), virtualCb: func() {}, } return dt } func (dt *DirectoryTree) OnVirtualNode(cb func()) { dt.virtualCb = cb } func (dt *DirectoryTree) ClearList() { dt.list = make([]*types.Thread, 0) } func (dt *DirectoryTree) Update(msg types.WorkerMessage) { switch msg := msg.(type) { case *types.Done: switch msg.InResponseTo().(type) { case *types.RemoveDirectory, *types.ListDirectories, *types.CreateDirectory: dt.DirectoryList.Update(msg) dt.buildTree() dt.Invalidate() default: dt.DirectoryList.Update(msg) } default: dt.DirectoryList.Update(msg) } } func (dt *DirectoryTree) Draw(ctx *ui.Context) { uiConfig := dt.UiConfig("") ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', 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 || dt.listIdx < 0 { style := uiConfig.GetStyle(config.STYLE_DIRLIST_DEFAULT) ctx.Printf(0, 0, style, 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 { return } treeCtx := ctx.Subcontext(0, 0, textWidth, ctx.Height()) data := state.NewDataSetter() data.SetAccount(dt.acctConf) n = 0 for i, node := range dt.list { if n > treeCtx.Height() { break } rowNr := dt.countVisible(dt.list[:i]) if rowNr < dt.Scroll() || !isVisible(node) { continue } path := dt.getDirectory(node) dir := dt.Directory(path) treeDir := &models.Directory{ Name: dt.displayText(node), } if dir != nil { treeDir.Role = dir.Role } data.SetFolder(treeDir) data.SetRUE([]string{path}, dt.GetRUECount) left, right, style := dt.renderDir( path, uiConfig, data.Data(), i == dt.listIdx, treeCtx.Width(), ) treeCtx.Printf(0, n, style, "%s %s", left, right) n++ } 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.NextPrev(1) case tcell.WheelUp: dt.NextPrev(-1) } } } func (dt *DirectoryTree) Clicked(x int, y int) (string, bool) { if dt.list == nil || len(dt.list) == 0 || dt.countVisible(dt.list) < y+dt.Scroll() { return "", false } visible := 0 for _, node := range dt.list { if isVisible(node) { visible++ } if visible == y+dt.Scroll()+1 { if path := dt.getDirectory(node); path != "" { return path, true } node.Hidden = !node.Hidden dt.Invalidate() return "", false } } return "", false } func (dt *DirectoryTree) SelectedMsgStore() (*lib.MessageStore, bool) { if dt.virtual { return nil, false } if findString(dt.treeDirs, dt.selected) < 0 { dt.buildTree() if idx := findString(dt.treeDirs, dt.selected); idx >= 0 { selIdx, node := dt.getTreeNode(uint32(idx)) if node != nil { makeVisible(node) dt.listIdx = selIdx } } } return dt.DirectoryList.SelectedMsgStore() } 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.virtual = false dt.Select(path) } else { dt.virtual = true dt.virtualCb() } } 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.DirectoryList.worker.PathSeparator()) return fmt.Sprintf("%s%s%s", threadPrefix(node, false, false), 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.DirectoryList.worker.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.DirectoryList.worker.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.DirectoryList.worker.PathSeparator()) if levels := countLevels(node); levels < len(elems) { if node.FirstChild != nil && (levels+1) < len(elems) { levels += 1 } strDir := strings.Join(elems[:levels], dt.DirectoryList.worker.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.DirectoryList.worker.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} dt.buildTreeNode(root, sTree, 0xFFFFFF, 1) 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.DirectoryList.worker.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 { log.Warnf("failed to walk tree: %v", err) } } } func (dt *DirectoryTree) buildTreeNode(node *types.Thread, stree [][]string, defaultUid uint32, depth int) { 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) path := dt.getDirectory(node) 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) if dt.UiConfig(path).DirListCollapse != 0 { node.Hidden = depth > dt.UiConfig(path).DirListCollapse } dt.buildTreeNode(nextNode, next, defaultUid, depth+1) } } 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 { log.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) string { if node == nil && node.FirstChild == nil { return "" } if node.Hidden { return "+" } return "" }