package widgets import ( "errors" "fmt" "strings" "sync" h "git.bullercodeworks.com/brian/expds/helpers" t "git.bullercodeworks.com/brian/tcell-widgets" th "git.bullercodeworks.com/brian/tcell-widgets/helpers" wh "git.bullercodeworks.com/brian/tcell-widgets/helpers" "github.com/gdamore/tcell" ) type TreeBrowser struct { id string title string style tcell.Style active bool visible bool focusable bool x, y int w, h int border []rune list []string listNodes []*TreeNode cursor int cursorWrap bool nodes []*TreeNode depthIndic string onChange func(*TreeNode) bool onSelect func(*TreeNode) bool keyMap *t.KeyMap vimMode bool searching bool searchStr string hintText string logger func(string, ...any) m sync.Mutex } var _ t.Widget = (*TreeBrowser)(nil) func NewTreeBrowser(id string, s tcell.Style) *TreeBrowser { ret := &TreeBrowser{id: id, style: s} ret.Init(id, s) return ret } func (w *TreeBrowser) Init(id string, style tcell.Style) { w.visible = true w.focusable = true w.depthIndic = "• " w.keyMap = t.NewKeyMap( 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.KeyEnter), func(ev *tcell.EventKey) bool { if w.searching { w.searching = !w.searching return true } if w.onSelect != nil { n, err := w.GetActiveNode() if err != nil || n == nil { return false } w.onSelect(n) return true } return false }), t.NewKey(t.BuildEK(tcell.KeyPgDn), func(_ *tcell.EventKey) bool { return w.PageDn() }), t.NewKey(t.BuildEK(tcell.KeyPgUp), func(_ *tcell.EventKey) bool { return w.PageUp() }), t.NewKey(t.BuildEKr('j'), func(ev *tcell.EventKey) bool { if !w.vimMode { return false } return w.MoveDown() }), t.NewKey(t.BuildEKr('k'), func(ev *tcell.EventKey) bool { if !w.vimMode { return false } return w.MoveUp() }), t.NewKey(t.BuildEKr('b'), func(ev *tcell.EventKey) bool { if !w.vimMode { return false } if ev.Modifiers()&tcell.ModCtrl != 0 { return w.PageUp() } return false }), t.NewKey(t.BuildEKr('f'), func(ev *tcell.EventKey) bool { if !w.vimMode { return false } if ev.Modifiers()&tcell.ModCtrl != 0 { return w.PageDn() } return false }), t.NewKey(t.BuildEKr('/'), func(ev *tcell.EventKey) bool { if !w.searching { w.searching = true w.searchStr = "" return true } return false }), ) } func (w *TreeBrowser) Id() string { return w.id } func (w *TreeBrowser) HandleResize(ev *tcell.EventResize) { w.w, w.h = ev.Size() } func (w *TreeBrowser) GetKeyMap() *t.KeyMap { return w.keyMap } func (w *TreeBrowser) SetKeyMap(km *t.KeyMap) { w.keyMap = km } func (w *TreeBrowser) HandleKey(ev *tcell.EventKey) bool { if !w.active || !w.focusable { return false } 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) Draw(screen tcell.Screen) { w.m.Lock() defer w.m.Unlock() if !w.visible { return } dS := w.style if !w.active { dS = dS.Dim(true) } x, y := w.x, w.y brdSz := 0 if len(w.border) > 0 { brdSz = 2 if len(w.title) > 0 { th.TitledBorderFilled(x, y, x+w.w, y+w.h, w.title, w.border, dS, screen) } else { th.BorderFilled(x, y, x+w.w, y+w.h, w.border, dS, screen) } } if w.hintText != "" { if brdSz == 0 { brdSz = 1 } wh.DrawText(x+1, y+w.h, w.hintText, dS, screen) } x, y = x+1, y+1 h := w.h - brdSz ln := len(w.list) st, ed := 0, ln-1 if ln == 0 { return } if ln > w.h-2 { mid := h / 2 if w.cursor < mid { // Start drawing at 0 ed = h } else if w.cursor > ln-mid { // Start at ln-h+1 st = ln - h } else { st = w.cursor - mid ed = st + h } } // ed cannot be higher than ln - 1 if st < 0 { st = 0 } if ed > ln-1 { ed = ln - 1 } for i := st; i <= ed; i++ { rev := false if i == w.cursor { rev = true } txt := w.list[i] if len(txt) > w.w-brdSz && w.w-brdSz >= 0 { txt = txt[:(w.w - brdSz)] } 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 } 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) Active() bool { return w.active } func (w *TreeBrowser) SetActive(a bool) bool { w.active = a return w.active } func (w *TreeBrowser) Visible() bool { return w.visible } func (w *TreeBrowser) SetVisible(a bool) { w.visible = a } func (w *TreeBrowser) Focusable() bool { return w.focusable } func (w *TreeBrowser) SetFocusable(b bool) { w.focusable = b } func (w *TreeBrowser) SetX(x int) { w.SetPos(t.Coord{X: x, Y: w.y}) } func (w *TreeBrowser) SetY(y int) { w.SetPos(t.Coord{X: w.x, Y: y}) } func (w *TreeBrowser) GetX() int { return w.x } func (w *TreeBrowser) GetY() int { return w.y } func (w *TreeBrowser) GetPos() t.Coord { return t.Coord{X: w.x, Y: w.y} } func (w *TreeBrowser) SetPos(c t.Coord) { w.x, w.y = c.X, c.Y } func (w *TreeBrowser) GetW() int { return w.w } func (w *TreeBrowser) GetH() int { return w.h } 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) SetSize(c t.Coord) { w.w, w.h = c.X, c.Y } func (w *TreeBrowser) WantW() int { w.m.Lock() defer w.m.Unlock() var want int for i := range w.list { want = h.MaxI(want, len(w.list[i])) } return w.w } func (w *TreeBrowser) WantH() int { w.m.Lock() defer w.m.Unlock() want := len(w.list) if len(w.border) > 0 { return want + 2 } return want } func (w *TreeBrowser) MinW() int { return w.w } func (w *TreeBrowser) MinH() int { return 5 } func (w *TreeBrowser) SetLogger(l func(string, ...any)) { w.logger = l } func (w *TreeBrowser) Log(txt string, args ...any) { w.logger(txt, args...) } func (w *TreeBrowser) SetBorder(brd []rune) { if len(brd) == 0 { w.border = wh.BRD_SIMPLE } else { w.border = wh.ValidateBorder(brd) } } func (w *TreeBrowser) ClearBorder() { w.border = []rune{} } 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) SetVimMode(b bool) { w.vimMode = b } func (w *TreeBrowser) GetNodeList() []*TreeNode { return w.nodes } func (w *TreeBrowser) nmGetActiveNode() (*TreeNode, error) { if len(w.listNodes) <= 0 { return nil, errors.New("no nodes") } if w.cursor < 0 { return w.listNodes[0], nil } if w.cursor >= len(w.listNodes) { return w.listNodes[len(w.listNodes)-1], 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) MoveUp() bool { if w.cursor > 0 { w.cursor-- if w.onChange != nil { n, err := w.GetActiveNode() if err == nil && n != nil { w.onChange(n) } } return true } else if w.cursorWrap { w.cursor = len(w.list) - 1 if w.onChange != nil { n, err := w.GetActiveNode() if err == nil && n != nil { w.onChange(n) } } return true } return false } func (w *TreeBrowser) MoveDown() bool { if w.cursor <= len(w.list)-2 { w.cursor++ if w.onChange != nil { n, err := w.GetActiveNode() if err == nil && n != nil { w.onChange(n) } } return true } else if w.cursorWrap { w.cursor = 0 if w.WantH() > w.cursor && w.onChange != nil { n, err := w.GetActiveNode() if err == nil && n != nil { w.onChange(n) } } return true } return false } func (w *TreeBrowser) PageUp() bool { w.cursor -= w.h if len(w.border) > 0 { w.cursor += 2 } if w.cursor < 0 { w.cursor = 0 } return true } func (w *TreeBrowser) PageDn() bool { w.cursor += w.h if len(w.border) > 0 { w.cursor -= 1 } if w.cursor > len(w.list)-1 { w.cursor = len(w.list) - 1 } return true } func (w *TreeBrowser) Title() string { return w.title } func (w *TreeBrowser) SetTitle(ttl string) { w.title = ttl } func (w *TreeBrowser) SetTree(l []*TreeNode) { w.m.Lock() defer w.m.Unlock() w.nodes = l w.nmUpdateList() } func (w *TreeBrowser) Clear() { w.m.Lock() defer w.m.Unlock() w.nodes = []*TreeNode{} w.nmUpdateList() } func (w *TreeBrowser) Add(n *TreeNode) { w.m.Lock() defer w.m.Lock() if n.depthIndic == "" { n.depthIndic = w.depthIndic } w.nodes = append(w.nodes, n) w.nmUpdateList() } // Update the list, intended to be called locally within other functions that // handle the mutex func (w *TreeBrowser) nmUpdateList() { w.list = []string{} w.listNodes = []*TreeNode{} for i := range w.nodes { w.list = append(w.list, w.nodes[i].getList()...) w.listNodes = append(w.listNodes, w.nodes[i].getVisibleNodeList()...) } if w.cursor >= len(w.list) { w.cursor = len(w.list) - 1 } if w.cursor <= 0 { w.cursor = 0 } } 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] } func (w *TreeBrowser) SetHintText(txt string) { w.hintText = txt } func (w *TreeBrowser) ClearHintText(txt string) { w.hintText = "" } /* * Tree Node */ type TreeNode struct { label string value string expanded bool parent *TreeNode children []*TreeNode depthIndic string } func NewTreeNode(l, v string) *TreeNode { return &TreeNode{ label: l, value: v, depthIndic: "• ", } } func (tn *TreeNode) Depth() int { if tn.parent == nil { return 0 } return tn.parent.Depth() + 1 } func (tn *TreeNode) Label() string { return tn.label } func (tn *TreeNode) Value() string { return tn.value } func (tn *TreeNode) GetLabelPath() []string { var path []string if tn.parent != nil { path = tn.parent.GetLabelPath() } return append(path, tn.Label()) } func (tn *TreeNode) GetValuePath() []string { var path []string if tn.parent != nil { path = tn.parent.GetValuePath() } return append(path, tn.Value()) } func (tn *TreeNode) getList() []string { pre := strings.Repeat(tn.depthIndic, tn.Depth()) ret := []string{fmt.Sprintf("%s%s", pre, tn.label)} if tn.expanded { for i := range tn.children { ret = append(ret, tn.children[i].getList()...) } } return ret } func (tn *TreeNode) getVisibleNodeList() []*TreeNode { ret := []*TreeNode{tn} if tn.expanded { for i := range tn.children { ret = append(ret, tn.children[i].getVisibleNodeList()...) } } 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) IsExpanded() bool { return tn.expanded } func (tn *TreeNode) Expand() { tn.expanded = true } func (tn *TreeNode) Collapse() { tn.expanded = false } func (tn *TreeNode) ToggleExpand() { tn.expanded = !tn.expanded } func (tn *TreeNode) AddChild(t *TreeNode, rest ...*TreeNode) { if t.depthIndic == "" { t.depthIndic = tn.depthIndic } t.parent = tn tn.children = append(tn.children, t) for i := range rest { if rest[i].depthIndic == "" { rest[i].depthIndic = tn.depthIndic } rest[i].parent = tn tn.children = append(tn.children, rest[i]) } } func (tn *TreeNode) HasChildren() bool { return len(tn.children) > 0 } func (tn *TreeNode) GetPath() []string { var path []string if tn.parent != nil { path = tn.parent.GetPath() } 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 }