package ui import ( "time" "git.bullercodeworks.com/brian/boltbrowser/models" "git.bullercodeworks.com/brian/boltbrowser/util" "git.bullercodeworks.com/brian/wandle" "github.com/nsf/termbox-go" ) type BoltTreePane struct { x, y int width int height int scrollRow int active bool buffer []string pathBuffer [][]string currentPath []string currentPathIdx int filter string filterMode bool insertPairCmd func(*models.BoltBucket) wandle.Cmd insertBucketCmd func(*models.BoltBucket) wandle.Cmd editPairKeyCmd func(*models.BoltPair) wandle.Cmd editPairValueCmd func(*models.BoltPair) wandle.Cmd editBucketCmd func(*models.BoltBucket) wandle.Cmd deleteItemCmd func([]string) wandle.Cmd setStatus func(string, time.Duration) db *models.BoltDB detailPane *BoltDetailPane } type TreePaneRenamePath struct { oldPath []string newPath []string } func NewBoltTreePane(x, y, w, h int) *BoltTreePane { return &BoltTreePane{ x: x, y: y, width: w, height: h, } } func (w *BoltTreePane) Init() wandle.Cmd { if w.db != nil { w.refreshDBAndBuffer() } return nil } func (w *BoltTreePane) SetDetailPane(pane *BoltDetailPane) { w.detailPane = pane } func (w *BoltTreePane) Measure() {} func (w *BoltTreePane) Update(msg wandle.Msg) wandle.Cmd { if w.db == nil { return nil } if len(w.currentPath) == 0 { w.currentPath = w.db.GetNextVisiblePath(nil, w.filter) } switch msg := msg.(type) { case BrowseMsg: if msg.source == BS_CmdRefreshTree { pth, ok := msg.data.(TreePaneRenamePath) if ok { if comparePaths(w.currentPath, pth.oldPath) { w.currentPath = pth.newPath } for i := range w.pathBuffer { if comparePaths(w.pathBuffer[i], pth.oldPath) { for j := range w.pathBuffer[i] { w.pathBuffer[i][j] = pth.newPath[j] } } } } w.refreshDBAndBuffer() } case termbox.Event: return w.handleTermboxEvent(msg) } return nil } func (w *BoltTreePane) handleTermboxEvent(msg termbox.Event) wandle.Cmd { switch msg.Type { case termbox.EventKey: if msg.Ch == 'g' { return w.jumpToStart } else if msg.Ch == 'G' { return w.jumpToEnd } else if msg.Key == termbox.KeyCtrlF { return w.pageDown } else if msg.Key == termbox.KeyCtrlB { return w.pageUp } else if msg.Ch == 'j' || msg.Key == termbox.KeyArrowDown { return w.moveCursorDown } else if msg.Ch == 'k' || msg.Key == termbox.KeyArrowUp { return w.moveCursorUp } else if msg.Key == termbox.KeyCtrlR { return w.refreshDBAndBuffer } else if msg.Ch == 'p' { return w.insertPairAtCurrent } else if msg.Ch == 'P' { return w.insertPairAtParent } else if msg.Ch == 'b' { return w.insertBucketAtCurrent } else if msg.Ch == 'B' { return w.insertBucketAtParent } else if msg.Ch == 'e' { return w.editPairValue } else if msg.Ch == 'r' { return w.renameBucketOrPair } else if msg.Key == termbox.KeyEnter { return w.handleEnterPressed } else if msg.Ch == 'l' || msg.Key == termbox.KeyArrowRight { return w.handleRightPressed } else if msg.Ch == 'h' || msg.Key == termbox.KeyArrowLeft { return w.handleLeftPressed } else if msg.Ch == 'D' { return w.handleDelete } else if msg.Ch == 'x' { // TODO: Export Value } else if msg.Ch == 'X' { // TODO: Export Key/Value (or Bucket) as JSON } else if msg.Ch == '/' { // TODO: Start Filtering } } return nil } func (w *BoltTreePane) View(style wandle.Style) { if w.db == nil { txt := "No Database Loaded" midX := (w.x + w.width/2) - len(txt)/2 midY := (w.y + w.height/2) - 2 wandle.Print(midX, midY, style, txt) return } // Find the cursor in the pane for k, v := range w.pathBuffer { if comparePaths(w.currentPath, v) { w.scrollRow = k break } } treeOffset := 0 maxCursor := w.height * 2 / 3 if w.scrollRow > maxCursor { treeOffset = w.scrollRow - maxCursor } if len(w.buffer) > 0 { kIdx := 0 for k := treeOffset; k < len(w.buffer); k++ { v := w.buffer[k] st := style if k < len(w.pathBuffer) && comparePaths(w.currentPath, w.pathBuffer[k]) { st = st.Invert() } pos := (w.y + kIdx - 1) bot := (w.y + w.height) if pos < bot { wandle.Print(w.x, pos, st, util.Stringify([]byte(v))) } kIdx++ } } } func (w *BoltTreePane) SetInsertPairCommand(cmd func(*models.BoltBucket) wandle.Cmd) { w.insertPairCmd = cmd } func (w *BoltTreePane) SetInsertBucketCommand(cmd func(*models.BoltBucket) wandle.Cmd) { w.insertBucketCmd = cmd } func (w *BoltTreePane) SetEditPairKeyCommand(cmd func(*models.BoltPair) wandle.Cmd) { w.editPairKeyCmd = cmd } func (w *BoltTreePane) SetEditPairValueCommand(cmd func(*models.BoltPair) wandle.Cmd) { w.editPairValueCmd = cmd } func (w *BoltTreePane) SetRenameBucketCommand(cmd func(*models.BoltBucket) wandle.Cmd) { w.editBucketCmd = cmd } func (w *BoltTreePane) SetDeleteItemCommand(cmd func([]string) wandle.Cmd) { w.deleteItemCmd = cmd } func (w *BoltTreePane) IsActive() bool { return w.active } func (w *BoltTreePane) SetActive(b bool) { w.active = b } func (w *BoltTreePane) Focusable() bool { return true } func (w *BoltTreePane) SetX(x int) { w.x = x } func (w *BoltTreePane) SetY(y int) { w.y = y } func (w *BoltTreePane) GetX() int { return w.x } func (w *BoltTreePane) GetY() int { return w.y } func (w *BoltTreePane) SetHeight(h int) { w.height = h } func (w *BoltTreePane) GetHeight() int { return w.height } func (w *BoltTreePane) SetWidth(wdt int) { w.height = wdt } func (w *BoltTreePane) GetWidth() int { return w.width } func (w *BoltTreePane) HasDB() bool { return w.db != nil } func (w *BoltTreePane) SetDB(db *models.BoltDB) { w.db = db w.detailPane.SetDB(db) w.refreshDBAndBuffer() } func (w *BoltTreePane) refreshDBAndBuffer() wandle.Msg { w.db.RefreshDatabase() w.RefreshBuffer() w.UpdateDetailPane() return nil } func (w *BoltTreePane) RefreshBuffer() { w.buffer = nil buckets := w.db.GetBuckets() for i := range buckets { w.buffer = append(w.buffer, buckets[i].Lines()...) } w.pathBuffer, _ = w.db.BuildVisiblePathSlice(w.filter) if len(w.currentPath) == 0 { w.currentPath = w.db.GetNextVisiblePath(nil, w.filter) } } func (w *BoltTreePane) jumpCursorDown(distance int) { findPath := w.currentPath for idx, pth := range w.pathBuffer { startJump := true for i := range pth { if len(w.currentPath) > i && pth[i] != w.currentPath[i] { startJump = false } } if startJump { distance-- if distance == 0 { w.currentPath = w.pathBuffer[len(w.pathBuffer)-1-idx] } } } isCurPath := true for i := range w.currentPath { if w.currentPath[i] != findPath[i] { isCurPath = false break } } if isCurPath { w.currentPath = w.db.GetNextVisiblePath(nil, w.filter) } w.UpdateDetailPane() } func (w *BoltTreePane) jumpCursorUp(distance int) { findPath := w.currentPath for idx, pth := range w.pathBuffer { startJump := true for i := range pth { if len(w.currentPath) > i && pth[i] != w.currentPath[i] { startJump = false } } if startJump { distance-- if distance == 0 { w.currentPath = w.pathBuffer[idx] break } } } isCurPath := true for i := range w.currentPath { if w.currentPath[i] != findPath[i] { isCurPath = false break } } if isCurPath { w.currentPath = w.db.GetNextVisiblePath(nil, w.filter) } w.UpdateDetailPane() } func (w *BoltTreePane) jumpToStart() wandle.Msg { w.currentPath = w.db.GetNextVisiblePath(nil, w.filter) w.UpdateDetailPane() return nil } func (w *BoltTreePane) jumpToEnd() wandle.Msg { w.currentPath = w.db.GetPrevVisiblePath(nil, w.filter) w.UpdateDetailPane() return nil } func (w *BoltTreePane) pageUp() wandle.Msg { w.jumpCursorDown(w.height / 2) w.UpdateDetailPane() return nil } func (w *BoltTreePane) pageDown() wandle.Msg { w.jumpCursorDown(w.height / 2) w.UpdateDetailPane() return nil } func (w *BoltTreePane) moveCursorUp() wandle.Msg { p := w.db.GetPrevVisiblePath(w.currentPath, w.filter) if p != nil { w.currentPath = p } w.UpdateDetailPane() return nil } func (w *BoltTreePane) moveCursorDown() wandle.Msg { p := w.db.GetNextVisiblePath(w.currentPath, w.filter) if p != nil { w.currentPath = p } w.UpdateDetailPane() return nil } func (w *BoltTreePane) handleEnterPressed() wandle.Msg { b, p, _ := w.GetSelected() // Select the current item if b != nil { if w.db.ToggleOpenBucket(w.currentPath) == nil { w.RefreshBuffer() } } else if p != nil { // Edit the pair if w.editPairValueCmd != nil { return w.editPairValueCmd(p) } } return nil } func (w *BoltTreePane) handleRightPressed() wandle.Msg { b, p, _ := w.GetSelected() // Select the current item if b != nil { if w.db.OpenBucket(w.currentPath) == nil { w.RefreshBuffer() } } else if p != nil { // Edit the pair if w.editPairValueCmd != nil { return w.editPairValueCmd(p) } } return nil } func (w *BoltTreePane) handleLeftPressed() wandle.Msg { b, _, e := w.GetSelected() if e == nil && b != nil && b.IsExpanded() { if err := w.db.CloseBucket(w.currentPath); err == nil { w.RefreshBuffer() } } else { if len(w.currentPath) > 1 { parent, err := w.db.GetBucketFromPath(w.currentPath[:len(w.currentPath)-1]) if err == nil { if w.db.CloseBucket(parent.GetPath()) == nil { w.currentPath = parent.GetPath() w.RefreshBuffer() } } } else { } } return nil } func (w *BoltTreePane) handleDelete() wandle.Msg { if w.deleteItemCmd != nil { return w.deleteItemCmd(w.currentPath) } return nil } func (w *BoltTreePane) insertBucketAtCurrent() wandle.Msg { if w.insertBucketCmd == nil { return nil } insertPath := w.currentPath b, p, e := w.GetSelected() if p != nil { insertPath = insertPath[:len(insertPath)-1] b, e = w.db.GetBucketFromPath(insertPath) } if e != nil { w.setStatus("Error inserting new pair. Invalid Path.", time.Second) return nil } return w.insertBucketCmd(b) } func (w *BoltTreePane) insertBucketAtParent() wandle.Msg { if w.insertBucketCmd == nil { return nil } insertPath := w.currentPath[:len(w.currentPath)-1] b, e := w.db.GetBucketFromPath(insertPath) if e != nil { w.setStatus("Error inserting new pair. Invalid Path.", time.Second) return nil } return w.insertBucketCmd(b) } func (w *BoltTreePane) insertPairAtCurrent() wandle.Msg { insertPath := w.currentPath b, p, e := w.GetSelected() if p != nil { insertPath = insertPath[:len(insertPath)-1] b, e = w.db.GetBucketFromPath(insertPath) } if e != nil { w.setStatus("Error inserting new pair. Invalid Path.", time.Second) return nil } return w.insertPairCmd(b) } func (w *BoltTreePane) insertPairAtParent() wandle.Msg { insertPath := w.currentPath[:len(w.currentPath)-1] b, e := w.db.GetBucketFromPath(insertPath) if e != nil { w.setStatus("Error inserting new pair. Invalid Path.", time.Second) return nil } return w.insertPairCmd(b) } func (w *BoltTreePane) editPairValue() wandle.Msg { if w.editPairValueCmd == nil { return nil } b, p, _ := w.GetSelected() if b != nil { w.setStatus("Cannot edit a bucket, did you mean to (r)ename?", time.Second*2) } else if p != nil { return w.editPairValueCmd(p) } return nil } func (w *BoltTreePane) renameBucketOrPair() wandle.Msg { b, p, e := w.GetSelected() if e != nil { w.setStatus("Error renaming entity", time.Second) } else if b != nil { return w.editBucketCmd(b) } else if p != nil { if w.editPairKeyCmd != nil { return w.editPairKeyCmd(p) } } return nil } func (w *BoltTreePane) SetStatusFunc(f func(string, time.Duration)) { w.setStatus = f } // We do this a lot, so here's a helper function func (w *BoltTreePane) GetCurrentPath() []string { return w.currentPath } func (w *BoltTreePane) GetSelected() (*models.BoltBucket, *models.BoltPair, error) { return w.db.GetGenericFromPath(w.currentPath) } func (w *BoltTreePane) UpdateDetailPane() { if w.detailPane != nil { w.detailPane.SetActiveItem(w.GetSelected()) } }