init: pristine aerc 0.20.0 source
This commit is contained in:
+543
@@ -0,0 +1,543 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/config"
|
||||
"git.sr.ht/~rjarry/aerc/lib"
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/lib/state"
|
||||
"git.sr.ht/~rjarry/aerc/lib/ui"
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||
"git.sr.ht/~rockorager/vaxis"
|
||||
)
|
||||
|
||||
type DirectoryTree struct {
|
||||
*DirectoryList
|
||||
|
||||
listIdx int
|
||||
list []*types.Thread
|
||||
|
||||
virtual bool
|
||||
virtualCb func()
|
||||
}
|
||||
|
||||
func NewDirectoryTree(dirlist *DirectoryList) DirectoryLister {
|
||||
dt := &DirectoryTree{
|
||||
DirectoryList: dirlist,
|
||||
listIdx: -1,
|
||||
virtualCb: func() {},
|
||||
}
|
||||
return dt
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) OnVirtualNode(cb func()) {
|
||||
dt.virtualCb = cb
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) Selected() string {
|
||||
if dt.listIdx < 0 || dt.listIdx >= len(dt.list) {
|
||||
return dt.DirectoryList.Selected()
|
||||
}
|
||||
node := dt.list[dt.listIdx]
|
||||
elems := dt.nodeElems(node)
|
||||
n := countLevels(node)
|
||||
if n < 0 || n >= len(elems) {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(elems[:(n+1)], dt.DirectoryList.worker.PathSeparator())
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) SelectedDirectory() *models.Directory {
|
||||
if dt.virtual {
|
||||
return &models.Directory{
|
||||
Name: dt.Selected(),
|
||||
Role: models.VirtualRole,
|
||||
}
|
||||
}
|
||||
return dt.DirectoryList.SelectedDirectory()
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) ClearList() {
|
||||
dt.list = make([]*types.Thread, 0)
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) Update(msg types.WorkerMessage) {
|
||||
selected := dt.Selected()
|
||||
switch msg := msg.(type) {
|
||||
case *types.Done:
|
||||
switch msg.InResponseTo().(type) {
|
||||
case *types.RemoveDirectory, *types.ListDirectories, *types.CreateDirectory:
|
||||
dt.DirectoryList.Update(msg)
|
||||
dt.buildTree()
|
||||
if selected != "" {
|
||||
dt.reindex(selected)
|
||||
}
|
||||
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, "%s", 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 vaxis.Event) {
|
||||
if event, ok := event.(vaxis.Mouse); ok {
|
||||
switch event.Button {
|
||||
case vaxis.MouseLeftButton:
|
||||
clickedDir, ok := dt.Clicked(localX, localY)
|
||||
if ok {
|
||||
dt.Select(clickedDir)
|
||||
}
|
||||
case vaxis.MouseWheelDown:
|
||||
dt.NextPrev(1)
|
||||
case vaxis.MouseWheelUp:
|
||||
dt.NextPrev(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) Clicked(x int, y int) (string, bool) {
|
||||
if 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
|
||||
}
|
||||
if node.Hidden == 0 {
|
||||
node.Hidden = 1
|
||||
} else {
|
||||
node.Hidden = 0
|
||||
}
|
||||
dt.Invalidate()
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) SelectedMsgStore() (*lib.MessageStore, bool) {
|
||||
if dt.virtual {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
selected := models.UID(dt.selected)
|
||||
if _, node := dt.getTreeNode(selected); node == nil {
|
||||
dt.buildTree()
|
||||
selIdx, node := dt.getTreeNode(selected)
|
||||
if node != nil {
|
||||
makeVisible(node)
|
||||
dt.listIdx = selIdx
|
||||
}
|
||||
}
|
||||
return dt.DirectoryList.SelectedMsgStore()
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) reindex(name string) {
|
||||
selIdx, node := dt.getTreeNode(models.UID(name))
|
||||
if node != nil {
|
||||
makeVisible(node)
|
||||
dt.listIdx = selIdx
|
||||
}
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) Select(name string) {
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
dt.Open(name, "", dt.UiConfig(name).DirListDelay, nil, false)
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) Open(name string, query string, delay time.Duration, cb func(types.WorkerMessage), force bool) {
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
again := false
|
||||
uid := models.UID(name)
|
||||
if _, node := dt.getTreeNode(uid); node == nil {
|
||||
again = true
|
||||
} else {
|
||||
dt.reindex(name)
|
||||
}
|
||||
dt.DirectoryList.Open(name, query, delay, func(msg types.WorkerMessage) {
|
||||
if cb != nil {
|
||||
cb(msg)
|
||||
}
|
||||
if _, ok := msg.(*types.Done); ok && again {
|
||||
if findString(dt.dirs, name) < 0 {
|
||||
dt.dirs = append(dt.dirs, name)
|
||||
}
|
||||
dt.buildTree()
|
||||
dt.reindex(name)
|
||||
}
|
||||
}, force)
|
||||
}
|
||||
|
||||
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.selectIndex(newIdx)
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) selectIndex(i int) {
|
||||
dt.listIdx = i
|
||||
node := dt.list[dt.listIdx]
|
||||
if node.Dummy {
|
||||
dt.virtual = true
|
||||
dt.NewContext()
|
||||
dt.virtualCb()
|
||||
} else {
|
||||
dt.virtual = false
|
||||
dt.Select(dt.getDirectory(node))
|
||||
}
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) CollapseFolder(name string) {
|
||||
name = strings.TrimRight(name, dt.worker.PathSeparator())
|
||||
index, node := dt.getTreeNode(models.UID(name))
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
if node.Parent != nil && (node.Hidden != 0 || node.FirstChild == nil) {
|
||||
node.Parent.Hidden = 1
|
||||
// highlight parent node and select it
|
||||
for i, t := range dt.list {
|
||||
if t == node.Parent && index == dt.listIdx {
|
||||
dt.selectIndex(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
node.Hidden = 1
|
||||
}
|
||||
dt.Invalidate()
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) ExpandFolder(name string) {
|
||||
name = strings.TrimRight(name, dt.worker.PathSeparator())
|
||||
_, node := dt.getTreeNode(models.UID(name))
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
node.Hidden = 0
|
||||
dt.Invalidate()
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) countVisible(list []*types.Thread) (n int) {
|
||||
for _, node := range list {
|
||||
if isVisible(node) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) nodeElems(node *types.Thread) []string {
|
||||
dir := string(node.Uid)
|
||||
sep := dt.DirectoryList.worker.PathSeparator()
|
||||
return strings.Split(dir, sep)
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) nodeName(node *types.Thread) string {
|
||||
if elems := dt.nodeElems(node); len(elems) > 0 {
|
||||
return elems[len(elems)-1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) displayText(node *types.Thread) string {
|
||||
return fmt.Sprintf("%s%s%s",
|
||||
threadPrefix(node, false, false),
|
||||
getFlag(node), dt.nodeName(node))
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) getDirectory(node *types.Thread) string {
|
||||
return string(node.Uid)
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) getTreeNode(uid models.UID) (int, *types.Thread) {
|
||||
for i, node := range dt.list {
|
||||
if node.Uid == uid {
|
||||
return i, node
|
||||
}
|
||||
}
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) hiddenDirectories() map[string]bool {
|
||||
hidden := make(map[string]bool, 0)
|
||||
for _, node := range dt.list {
|
||||
if node.Hidden != 0 && node.FirstChild != nil {
|
||||
elems := dt.nodeElems(node)
|
||||
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) {
|
||||
log.Tracef("setHiddenDirectories: %#v", hiddenDirs)
|
||||
for _, node := range dt.list {
|
||||
elems := dt.nodeElems(node)
|
||||
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 = 1
|
||||
log.Tracef("setHiddenDirectories: %q -> %#v", strDir, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) buildTree() {
|
||||
if len(dt.list) != 0 {
|
||||
hiddenDirs := dt.hiddenDirectories()
|
||||
defer dt.setHiddenDirectories(hiddenDirs)
|
||||
}
|
||||
|
||||
dirs := make([]string, len(dt.dirs))
|
||||
copy(dirs, dt.dirs)
|
||||
root := &types.Thread{}
|
||||
dt.buildTreeNode(root, dirs, 1)
|
||||
|
||||
var threads []*types.Thread
|
||||
for iter := root.FirstChild; iter != nil; iter = iter.NextSibling {
|
||||
iter.Parent = nil
|
||||
threads = append(threads, iter)
|
||||
}
|
||||
|
||||
// folders-sort
|
||||
if dt.DirectoryList.acctConf.EnableFoldersSort {
|
||||
sort.Slice(threads, func(i, j int) bool {
|
||||
foldersSort := dt.DirectoryList.acctConf.FoldersSort
|
||||
iInFoldersSort := findString(foldersSort, dt.getDirectory(threads[i]))
|
||||
jInFoldersSort := findString(foldersSort, dt.getDirectory(threads[j]))
|
||||
if iInFoldersSort >= 0 && jInFoldersSort >= 0 {
|
||||
return iInFoldersSort < jInFoldersSort
|
||||
}
|
||||
if iInFoldersSort >= 0 {
|
||||
return true
|
||||
}
|
||||
if jInFoldersSort >= 0 {
|
||||
return false
|
||||
}
|
||||
return dt.getDirectory(threads[i]) < dt.getDirectory(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, dirs []string, depth int) {
|
||||
dirmap := make(map[string][]string)
|
||||
for _, dir := range dirs {
|
||||
base, dir, cut := strings.Cut(
|
||||
dir, dt.DirectoryList.worker.PathSeparator())
|
||||
if _, found := dirmap[base]; found {
|
||||
if cut {
|
||||
dirmap[base] = append(dirmap[base], dir)
|
||||
}
|
||||
} else if cut {
|
||||
dirmap[base] = append(dirmap[base], dir)
|
||||
} else {
|
||||
dirmap[base] = []string{}
|
||||
}
|
||||
}
|
||||
bases := make([]string, 0, len(dirmap))
|
||||
for base, dirs := range dirmap {
|
||||
bases = append(bases, base)
|
||||
sort.Strings(dirs)
|
||||
}
|
||||
sort.Strings(bases)
|
||||
|
||||
basePath := dt.getDirectory(node)
|
||||
collapse := dt.UiConfig(basePath).DirListCollapse
|
||||
if collapse != 0 && depth > collapse {
|
||||
node.Hidden = 1
|
||||
} else {
|
||||
node.Hidden = 0
|
||||
}
|
||||
|
||||
for _, base := range bases {
|
||||
path := dt.childPath(basePath, base)
|
||||
nextNode := &types.Thread{Uid: models.UID(path)}
|
||||
|
||||
nextNode.Dummy = findString(dt.dirs, path) == -1
|
||||
|
||||
node.AddChild(nextNode)
|
||||
dt.buildTreeNode(nextNode, dirmap[base], depth+1)
|
||||
}
|
||||
}
|
||||
|
||||
func (dt *DirectoryTree) childPath(base, relpath string) string {
|
||||
if base == "" {
|
||||
return relpath
|
||||
}
|
||||
return base + dt.DirectoryList.worker.PathSeparator() + relpath
|
||||
}
|
||||
|
||||
func makeVisible(node *types.Thread) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
for iter := node.Parent; iter != nil; iter = iter.Parent {
|
||||
iter.Hidden = 0
|
||||
}
|
||||
}
|
||||
|
||||
func isVisible(node *types.Thread) bool {
|
||||
for iter := node.Parent; iter != nil; iter = iter.Parent {
|
||||
if iter.Hidden != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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 != 0 {
|
||||
return "+"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user