package main import ( "fmt" "path/filepath" "sort" "strings" "time" "unicode/utf8" "unsafe" _ "github.com/ying32/govcl/pkgs/winappres" // Extra _syso files for Windows "github.com/ying32/govcl/vcl" "github.com/ying32/govcl/vcl/types" "go.etcd.io/bbolt" ) type loadedDatabase struct { displayName string path string db *bbolt.DB nav *vcl.TTreeNode } type navData struct { ld *loadedDatabase childrenLoaded bool bucketPath []string } type TMainForm struct { *vcl.TForm Menu *vcl.TMainMenu Buckets *vcl.TTreeView Tabs *vcl.TPageControl propertiesBox *vcl.TMemo contentBox *vcl.TListView dbs []loadedDatabase arena []*navData // keepalive } var ( mainForm *TMainForm ) func main() { vcl.RunApp(&mainForm) } func (f *TMainForm) OnFormCreate(sender vcl.IObject) { const MY_SPACING = 6 const MY_WIDTH = 180 f.SetCaption("yvbolt") mnuFile := vcl.NewMenuItem(f) mnuFile.SetCaption("File") mnuFileOpen := vcl.NewMenuItem(mnuFile) mnuFileOpen.SetCaption("Open...") mnuFileOpen.SetShortCutFromString("Ctrl+O") mnuFileOpen.SetOnClick(f.OnMnuFileOpenClick) mnuFile.Add(mnuFileOpen) f.Menu = vcl.NewMainMenu(f) f.Menu.Items().Add(mnuFile) f.Buckets = vcl.NewTreeView(f) f.Buckets.SetParent(f) f.Buckets.SetAlign(types.AlLeft) f.Buckets.SetWidth(MY_WIDTH) f.Buckets.SetReadOnly(true) // prevent click to rename on nodes f.Buckets.SetOnExpanding(f.OnNavExpanding) f.Buckets.SetOnChange(f.OnNavChange) hsplit := vcl.NewSplitter(f) hsplit.SetParent(f) hsplit.SetAlign(types.AlLeft) hsplit.SetLeft(1) // Just needs to be further "over" than f.Buckets for auto-alignment f.Tabs = vcl.NewPageControl(f) f.Tabs.SetParent(f) f.Tabs.SetAlign(types.AlClient) propertiesTab := vcl.NewTabSheet(f.Tabs) propertiesTab.SetParent(f.Tabs) propertiesTab.SetCaption("Properties") f.propertiesBox = vcl.NewMemo(propertiesTab) f.propertiesBox.SetParent(propertiesTab) f.propertiesBox.BorderSpacing().SetAround(MY_SPACING) f.propertiesBox.SetAlign(types.AlClient) // fill remaining space f.propertiesBox.SetReadOnly(true) f.propertiesBox.SetEnabled(false) f.propertiesBox.SetBorderStyle(types.BsNone) f.propertiesBox.SetText("Open a database to get started...") dataTab := vcl.NewTabSheet(f.Tabs) dataTab.SetParent(f.Tabs) dataTab.SetCaption("Data") f.contentBox = vcl.NewListView(dataTab) f.contentBox.SetParent(dataTab) f.contentBox.BorderSpacing().SetAround(MY_SPACING) f.contentBox.SetAlign(types.AlClient) // fill remaining space f.contentBox.SetViewStyle(types.VsReport) // "Report style" i.e. has columns f.contentBox.SetAutoWidthLastColumn(true) f.contentBox.SetReadOnly(true) colKey := f.contentBox.Columns().Add() colKey.SetCaption("Key") colKey.SetWidth(MY_WIDTH) colKey.SetAlignment(types.TaLeftJustify) colVal := f.contentBox.Columns().Add() colVal.SetCaption("Value") } func (f *TMainForm) OnMnuFileOpenClick(sender vcl.IObject) { dlg := vcl.NewOpenDialog(f) dlg.SetTitle("Select a database file...") dlg.SetFilter("Bolt database|*.db|SQLite database|*.db3|All files|*.*") ret := dlg.Execute() // Fake blocking if ret { f.addDatabaseFromFile(dlg.FileName()) } } func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) { if node.Data() == nil { vcl.ShowMessage("unexpected nil data") return } ndata := (*navData)(node.Data()) // Load properties bucketDisplayName := strings.Join(ndata.bucketPath, `/`) content := fmt.Sprintf("Selected database: %#v\n\n\nSelected bucket: %q\n", ndata.ld.db.Stats(), bucketDisplayName) f.propertiesBox.SetText(content) // Load data f.contentBox.SetEnabled(false) f.contentBox.Clear() err := ndata.ld.db.View(func(tx *bbolt.Tx) error { b := boltTargetBucket(tx, ndata.bucketPath) if b == nil { // no such bucket return nil } // Valid f.contentBox.Clear() c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { dataEntry := f.contentBox.Items().Add() dataEntry.SetCaption(formatUtf8(k)) dataEntry.SubItems().Add(formatUtf8(v)) } return nil }) if err != nil { vcl.ShowMessage(fmt.Sprintf("Failed to load data for bucket %q: %s", bucketDisplayName, err.Error())) return } // Valid f.contentBox.SetEnabled(true) } func formatUtf8(in []byte) string { if !utf8.Valid(in) { return fmt.Sprintf("<>", in) } return string(in) } func boltTargetBucket(tx *bbolt.Tx, path []string) *bbolt.Bucket { // If we are already deep in buckets, go directly there to find children if len(path) == 0 { return nil } b := tx.Bucket([]byte(path[0])) if b == nil { return nil // unexpectedly missing } for i := 1; i < len(path); i += 1 { b = b.Bucket([]byte(path[i])) if b == nil { return nil // unexpectedly missing } } return b // OK } func boltChildBucketNames(db *bbolt.DB, path []string) ([]string, error) { var nextBucketNames []string err := db.View(func(tx *bbolt.Tx) error { // If we are already deep in buckets, go directly there to find children if len(path) > 0 { b := tx.Bucket([]byte(path[0])) if b == nil { return fmt.Errorf("Unexpected missing root bucket %q", path[0]) } for i := 1; i < len(path); i += 1 { b = b.Bucket([]byte(path[i])) if b == nil { return fmt.Errorf("Unexpected missing bucket %q", strings.Join(path[0:i], `/`)) } } // Find child buckets of this bucket b.ForEachBucket(func(bucketName []byte) error { nextBucketNames = append(nextBucketNames, string(bucketName)) return nil }) } else { // Find root bucket names return tx.ForEach(func(bucketName []byte, _ *bbolt.Bucket) error { nextBucketNames = append(nextBucketNames, string(bucketName)) return nil }) } // OK return nil }) if err != nil { return nil, err } sort.Strings(nextBucketNames) return nextBucketNames, nil } func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allowExpansion *bool) { if node.Data() == nil { vcl.ShowMessage("unexpected nil data") *allowExpansion = false return } ndata := (*navData)(node.Data()) if ndata.childrenLoaded { return } // Find the child buckets from this point under the element nextBucketNames, err := boltChildBucketNames(ndata.ld.db, ndata.bucketPath) if err != nil { vcl.ShowMessage(fmt.Sprintf("Failed to find child buckets under %q: %s", strings.Join(ndata.bucketPath, `/`), err.Error())) *allowExpansion = false return } ndata.childrenLoaded = true // don't repeat this work if len(nextBucketNames) == 0 { node.SetHasChildren(false) *allowExpansion = false } else { // Populate LCL child nodes for _, bucketName := range nextBucketNames { node := f.Buckets.Items().AddChild(node, formatUtf8([]byte(bucketName))) node.SetHasChildren(true) // dynamically populate in OnNavExpanding navData := &navData{ ld: ndata.ld, childrenLoaded: false, // will be loaded dynamically bucketPath: []string{}, // empty = root } navData.bucketPath = append(navData.bucketPath, ndata.bucketPath...) navData.bucketPath = append(navData.bucketPath, bucketName) node.SetData(unsafe.Pointer(navData)) f.arena = append(f.arena, navData) // keepalive } *allowExpansion = true } } func (f *TMainForm) addDatabaseFromFile(path string) { // TODO load in background thread to stop blocking the UI db, err := bbolt.Open(path, 0644, &bbolt.Options{Timeout: 1 * time.Second}) if err != nil { vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error())) return } entry := loadedDatabase{ path: path, displayName: filepath.Base(path), db: db, } entry.nav = f.Buckets.Items().Add(nil, entry.displayName) entry.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding navData := &navData{ ld: &entry, childrenLoaded: false, // will be loaded dynamically bucketPath: []string{}, // empty = root } entry.nav.SetData(unsafe.Pointer(navData)) f.dbs = append(f.dbs, entry) f.arena = append(f.arena, navData) // keepalive }