boltbrowser/ui/screen_browse.go

515 lines
14 KiB
Go

package ui
import (
"fmt"
"path/filepath"
"strings"
"time"
"git.bullercodeworks.com/brian/boltbrowser/models"
"git.bullercodeworks.com/brian/wandle"
"git.bullercodeworks.com/brian/widdles"
"github.com/nsf/termbox-go"
"github.com/spf13/viper"
bolt "go.etcd.io/bbolt"
)
const (
BS_CmdRefresh = BrowseId | iota
BS_CmdRefreshTree
BS_CmdDBTimeout
)
type BrowseMsg struct {
source int
data interface{}
err error
}
type browseScreen struct {
ui *Ui
dbPath string
db *models.BoltDB
status *widdles.Text
treePane *BoltTreePane
detailPane *BoltDetailPane
inputDialog *widdles.InputDialog
confirmDialog *widdles.ConfirmDialog
initialized bool
}
func NewBrowseScreen(u *Ui) *browseScreen {
return &browseScreen{
ui: u,
inputDialog: widdles.NewInputDialog("Edit", ""),
confirmDialog: widdles.NewConfirmDialog("Are you sure?", ""),
}
}
func (s *browseScreen) Init() wandle.Cmd {
if s.initialized {
return nil
}
w, h := termbox.Size()
dbs := viper.GetStringSlice("dbs")
dbidx := viper.GetInt("dbidx")
if len(dbs) <= dbidx {
return wandle.Quit
}
s.dbPath = dbs[dbidx]
s.treePane = NewBoltTreePane(0, 3, w/2, h-6)
s.treePane.SetInsertPairCommand(s.insertPair)
s.treePane.SetInsertBucketCommand(s.insertBucket)
s.treePane.SetEditPairKeyCommand(s.editPairKey)
s.treePane.SetEditPairValueCommand(s.editPairValue)
s.treePane.SetRenameBucketCommand(s.editBucket)
s.treePane.SetDeleteItemCommand(s.deleteItem)
s.treePane.SetStatusFunc(s.setStatus)
s.detailPane = &BoltDetailPane{
x: w/2 + 2, y: 2,
width: w / 2, height: h - 6,
}
s.detailPane.Init()
s.treePane.SetDetailPane(s.detailPane)
s.status = widdles.NewText("Press '?' for help", 0, (h - 1), w, 1)
s.inputDialog.Init()
s.confirmDialog.Init()
return func() wandle.Msg {
timeout, err := time.ParseDuration(viper.GetString("version"))
if err != nil {
timeout = time.Second
}
db, err := bolt.Open(s.dbPath, 0600, &bolt.Options{Timeout: timeout})
if err == bolt.ErrTimeout {
return func() wandle.Msg { return BrowseMsg{source: BS_CmdDBTimeout} }
}
s.db = models.NewBoltDB(db)
if viper.GetBool("readonly") {
db.Close()
}
s.treePane.SetDB(s.db)
s.resizeWindow(w, h)
s.initialized = true
return nil
}
}
func (s *browseScreen) Update(msg wandle.Msg) wandle.Cmd {
switch msg := msg.(type) {
case BrowseMsg:
return s.handleBrowseMsg(msg)
case termbox.Event:
return s.handleTermboxEvent(msg)
}
return nil
}
func (s *browseScreen) handleBrowseMsg(msg BrowseMsg) wandle.Cmd {
if msg.source == BS_CmdRefreshTree {
return s.treePane.Update(msg)
}
return nil
}
func (s *browseScreen) handleTermboxEvent(msg termbox.Event) wandle.Cmd {
if s.inputDialog.IsActive() {
return s.inputDialog.Update(msg)
}
if s.confirmDialog.IsActive() {
return s.confirmDialog.Update(msg)
}
switch msg.Type {
case termbox.EventKey:
if cmd := s.treePane.Update(msg); cmd != nil {
return cmd
}
switch {
case msg.Ch == '?':
return wandle.SwitchScreenCmd(NewAboutScreen(s.ui))
case msg.Ch == 'q' || msg.Key == termbox.KeyCtrlC:
return wandle.Quit
case msg.Ch == 'x':
return s.exportValue
case msg.Ch == 'X':
return s.exportJSON
// TODO: External editor
//case msg.Ch == 'E':
// return s.startEditor
case msg.Ch == 'J':
// TODO: Move Right Pane Down
case msg.Ch == 'K':
// TODO: Move Right Pane Up
case msg.Key == termbox.KeyCtrlN:
// Next File
idx := viper.GetInt("dbidx") + 1
if idx >= len(viper.GetStringSlice("dbs")) {
s.setStatus("Already at last file", time.Second)
} else {
viper.Set("dbidx", idx)
return wandle.SwitchScreenCmd(NewBrowseScreen(s.ui))
}
case msg.Key == termbox.KeyCtrlP:
// Previous File
idx := viper.GetInt("dbidx") - 1
if idx < 0 {
s.setStatus("Already at first file", time.Second)
} else {
viper.Set("dbidx", idx)
return wandle.SwitchScreenCmd(NewBrowseScreen(s.ui))
}
}
case termbox.EventResize:
s.resizeWindow(msg.Width, msg.Height)
}
return nil
}
func (s *browseScreen) resizeWindow(w, h int) {
lw := w
if lw > 80 {
lw = lw / 2
}
// Re-build Tree pane
s.treePane.SetWidth(lw)
s.treePane.SetHeight(h - 4)
// Re-build Right Pane buffer
s.detailPane.SetX(lw + 1)
s.detailPane.SetWidth(w - lw)
s.detailPane.SetHeight(h - 2)
// Re-build the input dialog
dlgWidth := 4
if w < 80 {
dlgWidth = 2
}
s.inputDialog.SetX((w / 2) - (w / (dlgWidth * 2)))
s.inputDialog.SetWidth(w / dlgWidth)
s.inputDialog.SetY((h / 2) - (h / 4))
s.inputDialog.SetHeight(4)
// Re-build the confirmation dialog
s.confirmDialog.SetX((w / 2) - (w / (dlgWidth * 2)))
s.confirmDialog.SetWidth(w / dlgWidth)
s.confirmDialog.SetY((h / 2) - (h / 4))
s.confirmDialog.SetHeight(4)
s.confirmDialog.EnableHotkeys()
}
func (s *browseScreen) View(style wandle.Style) {
w, h := termbox.Size()
s.drawHeader(style)
s.treePane.View(style)
midX := s.detailPane.GetX() - 1
if w > 80 {
termbox.SetCell(midX, 1, '╤', style.Foreground, style.Background)
wandle.Fill('│', midX, 2, midX, h-3, style)
s.detailPane.View(style)
}
wandle.Fill('═', 0, h-2, w, h-2, style)
termbox.SetCell(midX, h-2, '╧', style.Foreground, style.Background)
s.status.View(style)
if s.inputDialog.IsVisible() {
s.inputDialog.View(style)
}
if s.confirmDialog.IsVisible() {
s.confirmDialog.View(style)
}
}
func (s *browseScreen) drawHeader(style wandle.Style) {
width, _ := termbox.Size()
headerStringLen := func(fileName string) int {
return len("boltbrowser") + len(fileName) + 1
}
headerFileName := s.dbPath
if headerStringLen(headerFileName) > width {
headerFileName = filepath.Base(headerFileName)
}
headerString := "boltbrowser" + ": " + headerFileName
count := ((width - len(headerString)) / 2) + 1
if count < 0 {
count = 0
}
spaces := strings.Repeat(" ", count)
wandle.Print(0, 0, style, fmt.Sprintf("%s%s%s", spaces, headerString, spaces))
wandle.Fill('═', 0, 1, width, 1, style)
}
func (s *browseScreen) setStatus(status string, timeout time.Duration) {
s.status.SetText(status)
if timeout > 0 {
go func() {
time.Sleep(timeout)
if s.status.GetText() == status {
s.status.SetText("")
s.ui.wandle.Send(BS_CmdRefresh)
}
}()
}
}
func (s *browseScreen) insertPair(bucket *models.BoltBucket) wandle.Cmd {
title := fmt.Sprintf("New Pair: %s", pathToString(bucket.GetPath()))
s.inputDialog.SetTitle(title)
s.inputDialog.SetMessage("New Pair Key")
s.inputDialog.SetValue("")
s.inputDialog.SetOkCommand(func() wandle.Msg {
key := s.inputDialog.GetValue()
if key == "" {
s.setStatus("Pair key cannot be empty", time.Second)
} else {
s.inputDialog.SetMessage("New Pair Value")
s.inputDialog.SetValue("")
s.inputDialog.SetOkCommand(func() wandle.Msg {
val := s.inputDialog.GetValue()
if err := s.db.InsertPair(bucket.GetPath(), key, val); err != nil {
s.setStatus("Error inserting pair.", time.Second)
} else {
s.inputDialog.Hide()
return BrowseMsg{source: BS_CmdRefreshTree}
}
return nil
})
}
return nil
})
s.inputDialog.SetCancelCommand(func() wandle.Msg {
s.inputDialog.Hide()
return nil
})
s.inputDialog.Show()
return nil
}
func (s *browseScreen) insertBucket(bucket *models.BoltBucket) wandle.Cmd {
title := fmt.Sprintf("New Bucket: %s", pathToString(s.treePane.currentPath))
s.inputDialog.SetTitle(title)
s.inputDialog.SetValue("")
s.inputDialog.SetOkCommand(func() wandle.Msg {
key := s.inputDialog.GetValue()
if key == "" {
s.setStatus("Pair key cannot be empty", time.Second)
} else {
if err := s.db.InsertBucket(bucket.GetPath(), key); err != nil {
s.setStatus("Error inserting pair.", time.Second)
} else {
s.inputDialog.Hide()
return BrowseMsg{source: BS_CmdRefreshTree}
}
}
return nil
})
s.inputDialog.SetCancelCommand(func() wandle.Msg {
s.inputDialog.Hide()
return nil
})
s.inputDialog.Show()
return nil
}
func (s *browseScreen) editPairKey(pair *models.BoltPair) wandle.Cmd {
s.inputDialog.SetTitle(fmt.Sprintf("Input new value for '%s'", pair.GetKey()))
s.inputDialog.SetValue(pair.GetKey())
s.inputDialog.SetOkCommand(func() wandle.Msg {
v := s.inputDialog.GetValue()
if v == "" {
s.setStatus("No value given, did you mean to (d)elete?", time.Second)
} else if err := s.db.UpdatePairKey(s.treePane.currentPath, v); err != nil {
s.setStatus("Error changing pair key", time.Second)
} else {
s.treePane.currentPath[len(s.treePane.currentPath)-1] = v
s.inputDialog.Hide()
return BrowseMsg{source: BS_CmdRefreshTree}
}
return nil
})
s.inputDialog.SetCancelCommand(func() wandle.Msg {
s.inputDialog.Hide()
return nil
})
s.inputDialog.Show()
return nil
}
func (s *browseScreen) editPairValue(pair *models.BoltPair) wandle.Cmd {
s.inputDialog.SetTitle(fmt.Sprintf("Input new value for '%s'", pair.GetKey()))
s.inputDialog.SetValue(pair.GetValue())
s.inputDialog.SetOkCommand(func() wandle.Msg {
v := s.inputDialog.GetValue()
if v == "" {
s.setStatus("No value given, did you mean to (d)elete?", time.Second)
} else if err := s.treePane.db.UpdatePairValue(s.treePane.currentPath, v); err != nil {
s.setStatus("Error updating pair value.", time.Second)
} else {
s.inputDialog.Hide()
return BrowseMsg{source: BS_CmdRefreshTree}
}
return nil
})
s.inputDialog.SetCancelCommand(func() wandle.Msg {
s.inputDialog.Hide()
return nil
})
s.inputDialog.Show()
return nil
}
func (s *browseScreen) editBucket(bucket *models.BoltBucket) wandle.Cmd {
s.inputDialog.SetTitle(fmt.Sprintf("Rename Bucket '%s' to:", bucket.GetName()))
s.inputDialog.SetValue(bucket.GetName())
s.inputDialog.SetOkCommand(func() wandle.Msg {
v := s.inputDialog.GetValue()
if v == "" {
s.setStatus("A Bucket has to have a name.", time.Second)
} else if err := s.db.RenameBucket(s.treePane.currentPath, v); err != nil {
s.setStatus("Error renaming bucket.", time.Second)
} else {
bkt, _ := s.treePane.db.GetBucketFromPath(s.treePane.currentPath)
if bkt != nil {
bkt.SetName(v)
}
oldPath := make([]string, len(s.treePane.currentPath))
copy(oldPath, s.treePane.currentPath)
s.treePane.currentPath[len(s.treePane.currentPath)-1] = v
s.inputDialog.Hide()
return BrowseMsg{
source: BS_CmdRefreshTree,
data: TreePaneRenamePath{
oldPath: oldPath,
newPath: s.treePane.currentPath,
},
}
}
return nil
})
s.inputDialog.Show()
return nil
}
func (s *browseScreen) deleteItem(path []string) wandle.Cmd {
key := path[len(path)-1]
var itemType string
b, p, _ := s.treePane.db.GetGenericFromPath(path)
if b != nil {
itemType = "Bucket"
} else if p != nil {
itemType = "Pair"
} else {
s.setStatus("Error deleting item.", time.Second)
}
s.confirmDialog.SetTitle(fmt.Sprintf("Delete %s '%s'?", itemType, key))
s.confirmDialog.SetMessage(fmt.Sprintf("Are you sure you want to delete this %s?", itemType))
s.confirmDialog.SetOkCommand(func() wandle.Msg {
s.treePane.moveCursorUp()
if err := s.treePane.db.DeleteKey(path); err != nil {
s.setStatus(fmt.Sprintf("Error deleting %s.", itemType), time.Second)
}
s.confirmDialog.Hide()
return BrowseMsg{source: BS_CmdRefreshTree}
})
s.confirmDialog.SetCancelCommand(func() wandle.Msg {
s.confirmDialog.Hide()
return nil
})
s.confirmDialog.Show()
return nil
}
func (s *browseScreen) exportValue() wandle.Msg {
path := s.treePane.GetCurrentPath()
b, p, e := s.treePane.GetSelected()
if e != nil || p == nil {
s.setStatus("Couldn't do string export on "+path[len(path)-1]+" (did you mean 'X'?)", time.Second)
return nil
}
s.inputDialog.SetTitle(fmt.Sprintf("Export value of '%s' to:", b.GetName()))
s.inputDialog.SetValue("")
s.inputDialog.SetOkCommand(func() wandle.Msg {
v := s.inputDialog.GetValue()
if v == "" {
s.setStatus("Must give a file name to export to.", time.Second)
} else {
if err := s.db.ExportValue(s.treePane.GetCurrentPath(), v); err != nil {
s.setStatus("Error Exporting Value", time.Second)
}
}
return nil
})
s.inputDialog.Show()
return nil
}
func (s *browseScreen) exportJSON() wandle.Msg {
b, p, e := s.treePane.GetSelected()
if e != nil {
s.setStatus("Error getting value to export", time.Second)
return nil
}
var nm string
if b != nil {
nm = b.GetName()
} else if p != nil {
nm = p.GetKey()
}
s.inputDialog.SetTitle(fmt.Sprintf("Export JSON of '%s' to:", nm))
s.inputDialog.SetValue("")
s.inputDialog.SetOkCommand(func() wandle.Msg {
v := s.inputDialog.GetValue()
if v == "" {
s.setStatus("Must give a file name to export to.", time.Second)
} else {
if err := s.db.ExportJSON(s.treePane.GetCurrentPath(), v); err != nil {
s.setStatus("Error Exporting JSON", time.Second)
}
}
return nil
})
s.inputDialog.Show()
return nil
}
/*
* TODO: This kind of works, but we need to stop termbox from intercepting
* keypresses, and we need to make sure that the screen redraws when we
* return.
func (s *browseScreen) startEditor() wandle.Msg {
editor := os.Getenv("EDITOR")
if editor == "" {
s.setStatus("EDITOR environment variable is not set.", time.Second)
return nil
}
path := s.treePane.GetCurrentPath()
_, p, _ := s.treePane.GetSelected()
if p == nil {
s.setStatus("Error pulling value to write to temp file", time.Second)
return nil
}
f, err := os.CreateTemp(os.TempDir(), "boltbrowser-")
if err != nil {
return err
}
defer f.Close()
defer os.Remove(f.Name())
if _, err := f.Write([]byte(p.GetValue())); err != nil {
s.setStatus("Error writing value to temp file", time.Second)
return nil
}
c := exec.Command(editor, f.Name())
c.Stdin = os.Stdin
c.Stdout = os.Stdout
e := c.Run()
if e != nil {
return nil
}
if b, e := ioutil.ReadAll(f); e != nil {
s.setStatus("Error reading edited temp file", time.Second)
} else {
if err := s.treePane.db.UpdatePairValue(path, string(b)); err != nil {
s.setStatus("Error saving new value to db", time.Second)
}
}
return nil
}
*/