db39ca181a
Adds a setting to the configuration to choose at which level the folders in the dirtree are collapsed by default. In my case, this is useful because my organisation has some rather deep nesting in the folder structure, and a _lot_ of folders, and this way I can keep my dirtree uncluttered while still having all folders there if I need them. Signed-off-by: Sijmen <me@sijman.nl> Acked-by: Koni Marti <koni.marti@gmail.com>
457 lines
10 KiB
Go
457 lines
10 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}
|
|
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.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 (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)
|
|
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().DirListCollapse != 0 {
|
|
node.Hidden = depth > dt.UiConfig().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 {
|
|
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
|
|
}
|