Tree Browser Work

This commit is contained in:
2026-02-05 11:46:41 -06:00
parent 687e64e701
commit 367d62ff00
8 changed files with 286 additions and 39 deletions

View File

@@ -23,8 +23,10 @@ package app
import ( import (
"fmt" "fmt"
"log/slog"
"net/url" "net/url"
"strings" "strings"
"time"
"git.bullercodeworks.com/brian/expds/data" "git.bullercodeworks.com/brian/expds/data"
"git.bullercodeworks.com/brian/expds/data/models" "git.bullercodeworks.com/brian/expds/data/models"
@@ -43,8 +45,7 @@ type ScreenHome struct {
w, h int w, h int
style tcell.Style style tcell.Style
alert *w.Alert alert *w.Alert
showAlert bool
menuLayout *w.TopMenuLayout menuLayout *w.TopMenuLayout
@@ -164,9 +165,6 @@ func (s *ScreenHome) HandleResize(ev *tcell.EventResize) {
} }
func (s *ScreenHome) HandleKey(ev *tcell.EventKey) bool { func (s *ScreenHome) HandleKey(ev *tcell.EventKey) bool {
if s.showAlert {
return s.alert.HandleKey(ev)
}
if ev.Key() == tcell.KeyF12 { if ev.Key() == tcell.KeyF12 {
s.toggleCli() s.toggleCli()
return true return true
@@ -213,9 +211,6 @@ func (s *ScreenHome) Draw() {
s.a.DrawWidget(s.loading) s.a.DrawWidget(s.loading)
} }
s.a.DrawWidget(s.status) s.a.DrawWidget(s.status)
if s.showAlert {
s.a.DrawWidget(s.alert)
}
} }
func (s *ScreenHome) Exit() error { return nil } func (s *ScreenHome) Exit() error { return nil }
func (s *ScreenHome) Log(t string, a ...any) { func (s *ScreenHome) Log(t string, a ...any) {
@@ -316,9 +311,11 @@ func (s *ScreenHome) initCli() {
func (s *ScreenHome) toggleCli() { func (s *ScreenHome) toggleCli() {
if s.layout.Contains(s.cli) { if s.layout.Contains(s.cli) {
s.layout.Delete(s.cli) s.layout.Delete(s.cli)
s.layout.ActivateWidget(s.columns)
} else { } else {
s.layout.Add(s.cli) s.layout.Add(s.cli)
s.cli.SetVisible(true) s.cli.SetVisible(true)
s.layout.ActivateWidget(s.cli)
} }
} }
func (s *ScreenHome) hideCli() { func (s *ScreenHome) hideCli() {
@@ -344,7 +341,13 @@ func (s *ScreenHome) cliGetPds(args ...string) bool {
} }
go func() { go func() {
defer func() { s.isLoading = false }() defer func() { s.isLoading = false }()
pds, err := s.r.GetPDS(args[1]) var pds *models.Pds
var err error
if s.activePds != nil && string(s.activePds.AtId) == args[1] {
pds, err = s.r.ReloadPds(args[1])
} else {
pds, err = s.r.GetPDS(args[1])
}
if err != nil { if err != nil {
s.Log(err.Error()) s.Log(err.Error())
return return
@@ -353,7 +356,7 @@ func (s *ScreenHome) cliGetPds(args ...string) bool {
return return
} }
s.doOpen = false s.doOpen = false
s.cli.Log("Retrieved: %s (%s)", pds.AtId, pds.Did) slog.Default().Debug(fmt.Sprintf("Retrieved: %s (%s) at %s", pds.AtId, pds.Did, time.Now().Format(time.RFC3339)))
s.activePds = pds s.activePds = pds
s.updatePdsListing() s.updatePdsListing()
n, err := s.pdsListing.GetActiveNode() n, err := s.pdsListing.GetActiveNode()
@@ -388,9 +391,8 @@ func (s *ScreenHome) cliAuthPds(args ...string) bool {
if err != nil { if err != nil {
s.Log("Error starting auth flow: %w", err) s.Log("Error starting auth flow: %w", err)
} }
s.alert.SetTitle("Authentication Started") s.Log("Authentication Started")
s.alert.SetMessage(fmt.Sprintf("OAuth Process Started.\nIf a browser window didn't open, you can open this URL manually:\n%s", authUrl)) s.Log(fmt.Sprintf("OAuth Process Started.\nIf a browser window didn't open, you can open this URL manually:\n%s", strings.ReplaceAll(authUrl, "%", "%%")))
s.showAlert = true
}() }()
return true return true
} }
@@ -421,11 +423,14 @@ func (s *ScreenHome) cliSendStatus(args ...string) bool {
s.isLoading = true s.isLoading = true
go func() { go func() {
defer func() { s.isLoading = false }() defer func() { s.isLoading = false }()
if !s.r.Auth.HasAuth() { if !s.r.Auth.HasAuth(s.activePds.Did) {
s.Log("Not authorized. Run `authpds`") s.Log("Not authorized. Run `authpds`")
return return
} }
s.r.SendToPDS() err := s.r.SendToPDS(s.activePds.Did)
if err != nil {
s.Log("Error sending status: %w", err)
}
}() }()
return true return true
} }
@@ -434,6 +439,7 @@ func (s *ScreenHome) updatePdsListing() {
s.pdsListing.SetTitle(fmt.Sprintf("─ %s (%s)", s.activePds.AtId.String(), s.activePds.Did.String())) s.pdsListing.SetTitle(fmt.Sprintf("─ %s (%s)", s.activePds.AtId.String(), s.activePds.Did.String()))
s.pdsListing.Clear() s.pdsListing.Clear()
nsidList := s.activePds.NSIDStringList() nsidList := s.activePds.NSIDStringList()
var tree []*wd.TreeNode
for i, v := range nsidList { for i, v := range nsidList {
t := wd.NewTreeNode(v, v) t := wd.NewTreeNode(v, v)
nsid := s.activePds.NSIDs[i] nsid := s.activePds.NSIDs[i]
@@ -456,8 +462,9 @@ func (s *ScreenHome) updatePdsListing() {
c := wd.NewTreeNode(label, rIds[j]) c := wd.NewTreeNode(label, rIds[j])
t.AddChild(c) t.AddChild(c)
} }
s.pdsListing.Add(t) tree = append(tree, t)
} }
s.pdsListing.SetTree(tree)
s.layout.ActivateWidget(s.columns) s.layout.ActivateWidget(s.columns)
s.columns.ActivateWidget(s.pdsListing) s.columns.ActivateWidget(s.pdsListing)
s.pdsListing.SetActive(true) s.pdsListing.SetActive(true)

View File

@@ -41,6 +41,7 @@ var (
Name = "expds" Name = "expds"
cfgFile string cfgFile string
ConfigDir = "" ConfigDir = ""
debug = false
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "expds", Use: "expds",
Short: "Utility to edit and view PDS", Short: "Utility to edit and view PDS",
@@ -60,6 +61,7 @@ func Execute() {
func init() { func init() {
cobra.OnInitialize(initConfig) cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "debug mode")
} }
func initConfig() { func initConfig() {

View File

@@ -27,7 +27,7 @@ func (a *AppLogHandler) addGroups(groups ...string) { a.groups = append(a.group
// AppLogHandler can handle all levels // AppLogHandler can handle all levels
func (a *AppLogHandler) Enabled(_ context.Context, lvl slog.Level) bool { return lvl >= a.level } func (a *AppLogHandler) Enabled(_ context.Context, lvl slog.Level) bool { return lvl >= a.level }
func (a *AppLogHandler) Handle(ctx context.Context, rcd slog.Record) error { func (a *AppLogHandler) Handle(_ context.Context, rcd slog.Record) error {
if a.logFunc == nil { if a.logFunc == nil {
return errors.New("no log func defined") return errors.New("no log func defined")
} }

View File

@@ -40,6 +40,7 @@ type Repo struct {
handler *AppLogHandler handler *AppLogHandler
logFunc func(string, ...any) logFunc func(string, ...any)
Logger *slog.Logger
context context.Context context context.Context
} }
@@ -56,6 +57,7 @@ func NewRepo() (*Repo, error) {
} else { } else {
r.handler.SetLevel(slog.LevelWarn) r.handler.SetLevel(slog.LevelWarn)
} }
r.Logger = slog.Default()
var err error var err error
r.Auth, err = NewAuthRepo(r) r.Auth, err = NewAuthRepo(r)
if err != nil { if err != nil {
@@ -64,10 +66,7 @@ func NewRepo() (*Repo, error) {
return r, nil return r, nil
} }
func (r *Repo) GetPDS(atId string) (*models.Pds, error) { func (r *Repo) fetchPds(atId string) (*models.Pds, error) {
if p, ok := r.LoadedPDSs[atId]; ok && time.Since(p.RefreshTime) < r.BestBy {
return p, nil
}
p, err := models.NewPdsFromDid(atId) p, err := models.NewPdsFromDid(atId)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -76,8 +75,19 @@ func (r *Repo) GetPDS(atId string) (*models.Pds, error) {
return p, nil return p, nil
} }
func (r *Repo) SendToPDS() error { func (r *Repo) ReloadPds(atId string) (*models.Pds, error) {
session, err := r.Auth.GetSession() return r.fetchPds(atId)
}
func (r *Repo) GetPDS(atId string) (*models.Pds, error) {
if p, ok := r.LoadedPDSs[atId]; ok && time.Since(p.RefreshTime) < r.BestBy {
return p, nil
}
return r.fetchPds(atId)
}
func (r *Repo) SendToPDS(did syntax.DID) error {
session, err := r.Auth.GetSession(did)
if err != nil { if err != nil {
return err return err
} }
@@ -94,11 +104,11 @@ func (r *Repo) SendToPDS() error {
var resp struct { var resp struct {
Uri syntax.ATURI `json:"uri"` Uri syntax.ATURI `json:"uri"`
} }
slog.Debug("posting expds status...") r.Logger.Debug("posting expds status...")
if err := c.Post(r.context, "com.atproto.repo.CreateRecord", body, &resp); err != nil { if err := c.Post(r.context, "com.atproto.repo.CreateRecord", body, &resp); err != nil {
return err return err
} }
slog.Debug(fmt.Sprintf("posted: %s :: %s", resp.Uri.Authority(), resp.Uri.RecordKey())) r.Logger.Debug(fmt.Sprintf("posted: %s :: %s", resp.Uri.Authority(), resp.Uri.RecordKey()))
return nil return nil
} }
@@ -106,4 +116,6 @@ func (r *Repo) SetLogFunc(l func(string, ...any)) {
r.logFunc = l r.logFunc = l
r.handler = NewAppLogHandler(r.logFunc) r.handler = NewAppLogHandler(r.logFunc)
slog.SetDefault(slog.New(r.handler)) slog.SetDefault(slog.New(r.handler))
r.Logger = slog.Default()
r.Logger.Debug("New Log Func Set for slog")
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@@ -13,6 +14,7 @@ import (
"time" "time"
"github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/auth/oauth"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@@ -24,12 +26,15 @@ type AuthRepo struct {
context context.Context context context.Context
session *oauth.ClientSessionData session *oauth.ClientSessionData
Logger *slog.Logger
} }
func NewAuthRepo(r *Repo) (*AuthRepo, error) { func NewAuthRepo(r *Repo) (*AuthRepo, error) {
a := &AuthRepo{ a := &AuthRepo{
r: r, r: r,
context: r.context, context: r.context,
Logger: r.Logger,
} }
var err error var err error
a.oauthConfig, a.oauthClient, a.store, err = a.buildOAuthClient() a.oauthConfig, a.oauthClient, a.store, err = a.buildOAuthClient()
@@ -110,20 +115,22 @@ func (r *AuthRepo) ListenForCallback(res chan url.Values) (int, error) {
if !errors.Is(err, http.ErrServerClosed) { if !errors.Is(err, http.ErrServerClosed) {
panic(err) panic(err)
} }
r.Logger.Debug("Server Shut Down")
}() }()
return listener.Addr().(*net.TCPAddr).Port, nil return listener.Addr().(*net.TCPAddr).Port, nil
} }
func (r *AuthRepo) HasAuth() bool { func (r *AuthRepo) HasAuth(did syntax.DID) bool {
sess, err := r.store.GetMostRecentSession(r.context) sess, err := r.GetSession(did)
return err != nil || sess == nil return err == nil && sess != nil
} }
func (r *AuthRepo) GetSession() (*oauth.ClientSession, error) { func (r *AuthRepo) GetSession(did syntax.DID) (*oauth.ClientSession, error) {
sess, err := r.store.GetMostRecentSession(r.context) sess, err := r.store.GetMostRecentSessionFor(r.context, did)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting most recent session: %w", err) return nil, fmt.Errorf("error getting most recent session: %w", err)
} }
r.Logger.Warn(fmt.Sprintf("GetSession(): Resuming Session: %s (%s)", sess.SessionID, sess.AccountDID))
return r.oauthClient.ResumeSession(r.context, sess.AccountDID, sess.SessionID) return r.oauthClient.ResumeSession(r.context, sess.AccountDID, sess.SessionID)
} }

View File

@@ -96,6 +96,20 @@ func (m *SqliteStore) GetSession(ctx context.Context, did syntax.DID, sessionID
return &row.Data, nil return &row.Data, nil
} }
func (m *SqliteStore) GetMostRecentSessionFor(ctx context.Context, did syntax.DID) (*oauth.ClientSessionData, error) {
var row storedSessionData
res := m.db.WithContext(ctx).Where(&storedSessionData{
AccountDid: did,
}).Order(clause.OrderByColumn{
Column: clause.Column{Name: "updated_at"},
Desc: true,
}).First(&row)
if res.Error != nil {
return nil, res.Error
}
return &row.Data, nil
}
// not part of the ClientAuthStore interface, just used for the CLI app // not part of the ClientAuthStore interface, just used for the CLI app
func (m *SqliteStore) GetMostRecentSession(ctx context.Context) (*oauth.ClientSessionData, error) { func (m *SqliteStore) GetMostRecentSession(ctx context.Context) (*oauth.ClientSessionData, error) {
var row storedSessionData var row storedSessionData

View File

@@ -114,7 +114,7 @@ func (b *StatusBlock) DrawPath(screen tcell.Screen) {
x := b.x x := b.x
wh.DrawText(x, b.y, b.end, b.style, screen) wh.DrawText(x, b.y, b.end, b.style, screen)
x++ x++
pts := fmt.Sprintf(" %s ", strings.Join(b.parts, b.sep)) pts := fmt.Sprintf(" %s ", strings.Join(b.parts, b.sep))
wh.DrawText(x, b.y, pts, b.style, screen) wh.DrawText(x, b.y, pts, b.style, screen)
x += len(pts) - len(b.parts) x += len(pts) - len(b.parts)
wh.DrawText(x, b.y, b.end, b.style.Reverse(true), screen) wh.DrawText(x, b.y, b.end, b.style.Reverse(true), screen)

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"sync"
h "git.bullercodeworks.com/brian/expds/helpers" h "git.bullercodeworks.com/brian/expds/helpers"
t "git.bullercodeworks.com/brian/tcell-widgets" t "git.bullercodeworks.com/brian/tcell-widgets"
@@ -36,7 +37,12 @@ type TreeBrowser struct {
keyMap *t.KeyMap keyMap *t.KeyMap
vimMode bool vimMode bool
searching bool
searchStr string
logger func(string, ...any) logger func(string, ...any)
m sync.Mutex
} }
var _ t.Widget = (*TreeBrowser)(nil) var _ t.Widget = (*TreeBrowser)(nil)
@@ -55,6 +61,10 @@ func (w *TreeBrowser) Init(id string, style tcell.Style) {
t.NewKey(t.BuildEK(tcell.KeyUp), func(_ *tcell.EventKey) bool { return w.MoveUp() }), t.NewKey(t.BuildEK(tcell.KeyUp), func(_ *tcell.EventKey) bool { return w.MoveUp() }),
t.NewKey(t.BuildEK(tcell.KeyDown), func(_ *tcell.EventKey) bool { return w.MoveDown() }), t.NewKey(t.BuildEK(tcell.KeyDown), func(_ *tcell.EventKey) bool { return w.MoveDown() }),
t.NewKey(t.BuildEK(tcell.KeyEnter), func(ev *tcell.EventKey) bool { t.NewKey(t.BuildEK(tcell.KeyEnter), func(ev *tcell.EventKey) bool {
if w.searching {
w.searching = !w.searching
return true
}
if w.onSelect != nil { if w.onSelect != nil {
n, err := w.GetActiveNode() n, err := w.GetActiveNode()
if err != nil || n == nil { if err != nil || n == nil {
@@ -97,6 +107,14 @@ func (w *TreeBrowser) Init(id string, style tcell.Style) {
} }
return false return false
}), }),
t.NewKey(t.BuildEKr('/'), func(ev *tcell.EventKey) bool {
if !w.searching {
w.searching = true
w.searchStr = ""
return true
}
return false
}),
) )
} }
@@ -111,12 +129,20 @@ func (w *TreeBrowser) HandleKey(ev *tcell.EventKey) bool {
if !w.active || !w.focusable { if !w.active || !w.focusable {
return false return false
} }
return w.keyMap.Handle(ev) if w.keyMap.Handle(ev) {
return true
} else if w.searching {
w.updateSearch(ev)
return true
}
return false
} }
func (w *TreeBrowser) HandleTime(ev *tcell.EventTime) {} func (w *TreeBrowser) HandleTime(ev *tcell.EventTime) {}
func (w *TreeBrowser) Draw(screen tcell.Screen) { func (w *TreeBrowser) Draw(screen tcell.Screen) {
w.m.Lock()
defer w.m.Unlock()
if !w.visible { if !w.visible {
return return
} }
@@ -170,9 +196,23 @@ func (w *TreeBrowser) Draw(screen tcell.Screen) {
if len(txt) > w.w-brdSz && w.w-brdSz >= 0 { if len(txt) > w.w-brdSz && w.w-brdSz >= 0 {
txt = txt[:(w.w - brdSz)] txt = txt[:(w.w - brdSz)]
} }
wh.DrawText(x, y, txt, dS.Reverse(rev), screen) if w.searching && strings.Contains(txt, w.searchStr) {
// TODO: Fix multi-byte depth indicator
srchIdx := strings.Index(txt, w.searchStr)
endSrchIdx := srchIdx + len(w.searchStr)
wh.DrawText(x, y, txt[:srchIdx], dS.Reverse(rev), screen)
wh.DrawText(x+srchIdx, y, txt[srchIdx:endSrchIdx], dS.Reverse(!rev), screen)
if len(txt) > endSrchIdx {
wh.DrawText(x+endSrchIdx, y, txt[endSrchIdx:], dS.Reverse(rev), screen)
}
} else {
wh.DrawText(x, y, txt, dS.Reverse(rev), screen)
}
y += 1 y += 1
} }
if w.searching {
wh.DrawText(w.x, w.y+w.h, fmt.Sprintf("Searching: %s", w.searchStr), dS, screen)
}
} }
func (w *TreeBrowser) SetStyle(s tcell.Style) { w.style = s } func (w *TreeBrowser) SetStyle(s tcell.Style) { w.style = s }
@@ -197,6 +237,8 @@ func (w *TreeBrowser) SetW(wd int) { w.SetSize(t.Coord{X: wd, Y: w.h}) }
func (w *TreeBrowser) SetH(h int) { w.SetSize(t.Coord{X: w.w, Y: h}) } func (w *TreeBrowser) SetH(h int) { w.SetSize(t.Coord{X: w.w, Y: h}) }
func (w *TreeBrowser) SetSize(c t.Coord) { w.w, w.h = c.X, c.Y } func (w *TreeBrowser) SetSize(c t.Coord) { w.w, w.h = c.X, c.Y }
func (w *TreeBrowser) WantW() int { func (w *TreeBrowser) WantW() int {
w.m.Lock()
defer w.m.Unlock()
var want int var want int
for i := range w.list { for i := range w.list {
want = h.MaxI(want, len(w.list[i])) want = h.MaxI(want, len(w.list[i]))
@@ -204,6 +246,8 @@ func (w *TreeBrowser) WantW() int {
return w.w return w.w
} }
func (w *TreeBrowser) WantH() int { func (w *TreeBrowser) WantH() int {
w.m.Lock()
defer w.m.Unlock()
want := len(w.list) want := len(w.list)
if len(w.border) > 0 { if len(w.border) > 0 {
return want + 2 return want + 2
@@ -225,8 +269,8 @@ func (w *TreeBrowser) ClearBorder() { w.border = []rune{}
func (w *TreeBrowser) SetOnChange(c func(*TreeNode) bool) { w.onChange = c } func (w *TreeBrowser) SetOnChange(c func(*TreeNode) bool) { w.onChange = c }
func (w *TreeBrowser) SetOnSelect(s func(*TreeNode) bool) { w.onSelect = s } func (w *TreeBrowser) SetOnSelect(s func(*TreeNode) bool) { w.onSelect = s }
func (w *TreeBrowser) SetVimMode(b bool) { w.vimMode = b } func (w *TreeBrowser) SetVimMode(b bool) { w.vimMode = b }
func (w *TreeBrowser) GetActiveNode() (*TreeNode, error) { func (w *TreeBrowser) nmGetActiveNode() (*TreeNode, error) {
if len(w.listNodes) < 0 { if len(w.listNodes) <= 0 {
return nil, errors.New("no nodes") return nil, errors.New("no nodes")
} }
if w.cursor < 0 { if w.cursor < 0 {
@@ -237,6 +281,11 @@ func (w *TreeBrowser) GetActiveNode() (*TreeNode, error) {
} }
return w.listNodes[w.cursor], nil return w.listNodes[w.cursor], nil
} }
func (w *TreeBrowser) GetActiveNode() (*TreeNode, error) {
w.m.Lock()
defer w.m.Unlock()
return w.nmGetActiveNode()
}
func (w *TreeBrowser) SetCursorWrap(b bool) { w.cursorWrap = b } func (w *TreeBrowser) SetCursorWrap(b bool) { w.cursorWrap = b }
func (w *TreeBrowser) MoveUp() bool { func (w *TreeBrowser) MoveUp() bool {
if w.cursor > 0 { if w.cursor > 0 {
@@ -305,22 +354,30 @@ func (w *TreeBrowser) PageDn() bool {
func (w *TreeBrowser) Title() string { return w.title } func (w *TreeBrowser) Title() string { return w.title }
func (w *TreeBrowser) SetTitle(ttl string) { w.title = ttl } func (w *TreeBrowser) SetTitle(ttl string) { w.title = ttl }
func (w *TreeBrowser) SetTree(l []*TreeNode) { func (w *TreeBrowser) SetTree(l []*TreeNode) {
w.m.Lock()
defer w.m.Unlock()
w.nodes = l w.nodes = l
w.UpdateList() w.nmUpdateList()
} }
func (w *TreeBrowser) Clear() { func (w *TreeBrowser) Clear() {
w.m.Lock()
defer w.m.Unlock()
w.nodes = []*TreeNode{} w.nodes = []*TreeNode{}
w.UpdateList() w.nmUpdateList()
} }
func (w *TreeBrowser) Add(n *TreeNode) { func (w *TreeBrowser) Add(n *TreeNode) {
w.m.Lock()
defer w.m.Lock()
if n.depthIndic == "" { if n.depthIndic == "" {
n.depthIndic = w.depthIndic n.depthIndic = w.depthIndic
} }
w.nodes = append(w.nodes, n) w.nodes = append(w.nodes, n)
w.UpdateList() w.nmUpdateList()
} }
func (w *TreeBrowser) UpdateList() { // Update the list, intended to be called locally within other functions that
// handle the mutex
func (w *TreeBrowser) nmUpdateList() {
w.list = []string{} w.list = []string{}
w.listNodes = []*TreeNode{} w.listNodes = []*TreeNode{}
for i := range w.nodes { for i := range w.nodes {
@@ -335,6 +392,98 @@ func (w *TreeBrowser) UpdateList() {
} }
} }
func (w *TreeBrowser) UpdateList() {
w.m.Lock()
defer w.m.Unlock()
w.nmUpdateList()
}
func (w *TreeBrowser) nmSetNodeActive(tn *TreeNode) {
// Make sure that the selected node is visible
wrk := tn.parent
for wrk != nil {
wrk.expanded = true
wrk = wrk.parent
}
w.nmUpdateList()
for i := range w.listNodes {
if w.listNodes[i] == tn {
w.cursor = i
return
}
}
}
func (w *TreeBrowser) SetNodeActive(tn *TreeNode) {
w.m.Lock()
defer w.m.Unlock()
w.nmSetNodeActive(tn)
}
func (w *TreeBrowser) updateSearch(ev *tcell.EventKey) {
w.m.Lock()
defer w.m.Unlock()
if len(w.nodes) == 0 {
return
}
if wh.IsBS(*ev) {
if len(w.searchStr) > 0 {
w.searchStr = w.searchStr[:len(w.searchStr)-1]
if len(w.searchStr) == 0 {
w.searching = false
}
}
return
}
w.searchStr = fmt.Sprintf("%s%s", w.searchStr, string(ev.Rune()))
wrk, _ := w.nmGetActiveNode()
if wrk == nil {
wrk = w.nodes[0]
}
// Check the ative node & it's children for the search
if fnd := wrk.SearchLabels(w.searchStr); fnd != nil {
w.nmSetNodeActive(fnd)
return
}
// Didn't find a child of the active node that matched, look for a sibling
if wrk.parent != nil {
if fnd := wrk.parent.SearchLabels(w.searchStr); fnd != nil {
w.nmSetNodeActive(fnd)
return
}
}
// Check the next browser node
var stIdx int
for i := range w.nodes {
if w.nodes[i] == wrk {
stIdx = i + 1
break
}
}
for i := 0; i < len(w.nodes); i++ {
idx := (i + stIdx) % len(w.nodes)
if fnd := w.nodes[idx].SearchLabels(w.searchStr); fnd != nil {
w.nmSetNodeActive(fnd)
return
}
}
}
func (w *TreeBrowser) getCurrentLine() string {
w.m.Lock()
defer w.m.Unlock()
l := len(w.list)
if l == 0 {
return ""
}
if w.cursor < 0 || w.cursor >= l {
return ""
}
return w.list[w.cursor]
}
/* /*
* Tree Node * Tree Node
*/ */
@@ -370,6 +519,7 @@ func (tn *TreeNode) GetLabelPath() []string {
} }
return append(path, tn.Label()) return append(path, tn.Label())
} }
func (tn *TreeNode) getList() []string { func (tn *TreeNode) getList() []string {
pre := strings.Repeat(tn.depthIndic, tn.Depth()) pre := strings.Repeat(tn.depthIndic, tn.Depth())
ret := []string{fmt.Sprintf("%s%s", pre, tn.label)} ret := []string{fmt.Sprintf("%s%s", pre, tn.label)}
@@ -380,6 +530,7 @@ func (tn *TreeNode) getList() []string {
} }
return ret return ret
} }
func (tn *TreeNode) getVisibleNodeList() []*TreeNode { func (tn *TreeNode) getVisibleNodeList() []*TreeNode {
ret := []*TreeNode{tn} ret := []*TreeNode{tn}
if tn.expanded { if tn.expanded {
@@ -390,6 +541,19 @@ func (tn *TreeNode) getVisibleNodeList() []*TreeNode {
return ret return ret
} }
func (tn *TreeNode) SearchLabels(f string) *TreeNode {
if strings.Contains(tn.label, f) {
return tn
}
for i := 0; i < len(tn.children); i++ {
fnd := tn.children[i].SearchLabels(f)
if fnd != nil {
return fnd
}
}
return nil
}
func (tn *TreeNode) ToggleExpand() { tn.expanded = !tn.expanded } func (tn *TreeNode) ToggleExpand() { tn.expanded = !tn.expanded }
func (tn *TreeNode) AddChild(t *TreeNode, rest ...*TreeNode) { func (tn *TreeNode) AddChild(t *TreeNode, rest ...*TreeNode) {
@@ -415,3 +579,44 @@ func (tn *TreeNode) GetPath() []string {
} }
return append(path, tn.value) return append(path, tn.value)
} }
func (tn *TreeNode) GetChildren() []*TreeNode { return tn.children }
func (tn *TreeNode) GetFirstChild() *TreeNode {
if !tn.HasChildren() {
return nil
}
return tn.children[0]
}
func (tn *TreeNode) GetPrevChild(before *TreeNode) *TreeNode {
if !tn.HasChildren() {
return nil
}
var found bool
for i := len(tn.children) - 1; i >= 0; i-- {
if found {
return tn.children[i]
}
if tn.children[i] == before {
found = true
}
}
return nil
}
func (tn *TreeNode) GetNextChild(after *TreeNode) *TreeNode {
if !tn.HasChildren() {
return nil
}
var found bool
for i := range tn.children {
if found {
return tn.children[i]
}
if tn.children[i] == after {
found = true
}
}
return nil
}