diff options
Diffstat (limited to 'app/dirtree.go')
-rw-r--r-- | app/dirtree.go | 495 |
1 files changed, 495 insertions, 0 deletions
diff --git a/app/dirtree.go b/app/dirtree.go new file mode 100644 index 00000000..24e776d8 --- /dev/null +++ b/app/dirtree.go @@ -0,0 +1,495 @@ +package app + +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 "" +} |