package main import ( "errors" "fmt" "runtime" "runtime/debug" "strings" "unsafe" "github.com/pkg/browser" "github.com/ying32/govcl/vcl" "github.com/ying32/govcl/vcl/types" ) const ( APPNAME = "yvbolt" HOMEPAGE_URL = "https://code.ivysaur.me/yvbolt" ) type TMainForm struct { *vcl.TForm ImageList *vcl.TImageList Menu *vcl.TMainMenu SQLiteUseCliDriver *vcl.TMenuItem StatusBar *vcl.TStatusBar Buckets *vcl.TTreeView Tabs *vcl.TPageControl propertiesBox *vcl.TMemo contentBox *vcl.TListView queryInput *vcl.TMemo queryResult *vcl.TListView none *noLoadedDatabase dbs []loadedDatabase } var ( mainForm *TMainForm ) func main() { vcl.RunApp(&mainForm) } func (f *TMainForm) OnFormCreate(sender vcl.IObject) { f.ImageList = loadImages(f) f.SetCaption(APPNAME) f.ScreenCenter() f.SetWidth(1280) f.SetHeight(640) f.ImageList.GetIcon(imgDatabaseLightning, f.Icon()) mnuFile := vcl.NewMenuItem(f) mnuFile.SetCaption("File") // mnuFileBadger := vcl.NewMenuItem(mnuFile) mnuFileBadger.SetCaption("Badger") mnuFileBadger.SetImageIndex(imgVendorDgraph) mnuFile.Add(mnuFileBadger) vcl_menuitem(mnuFileBadger, "Open database...", imgDatabaseAdd, f.OnMnuFileBadgerOpenClick) vcl_menuitem(mnuFileBadger, "New in-memory database", imgDatabaseAdd, f.OnMnuFileBadgerMemoryClick) // mnuFileBolt := vcl.NewMenuItem(mnuFile) mnuFileBolt.SetCaption("Bolt") mnuFileBolt.SetImageIndex(imgVendorGithub) mnuFile.Add(mnuFileBolt) vcl_menuitem(mnuFileBolt, "New database...", imgDatabaseAdd, f.OnMnuFileBoltNewClick) vcl_menuitem(mnuFileBolt, "Open database...", imgDatabaseAdd, f.OnMnuFileBoltOpenClick) vcl_menuitem(mnuFileBolt, "Open database (read-only)...", imgDatabaseAdd, f.OnMnuFileBoltOpenReadonlyClick) // mnuFileDebconf := vcl.NewMenuItem(mnuFile) mnuFileDebconf.SetCaption("Debconf") mnuFileDebconf.SetImageIndex(imgVendorDebian) mnuFile.Add(mnuFileDebconf) vcl_menuitem(mnuFileDebconf, "Open database...", imgDatabaseAdd, f.OnMnuFileDebianOpenClick) // mnuFilePebble := vcl.NewMenuItem(mnuFile) mnuFilePebble.SetCaption("Pebble") mnuFilePebble.SetImageIndex(imgVendorCockroach) mnuFile.Add(mnuFilePebble) vcl_menuitem(mnuFilePebble, "Open database...", imgDatabaseAdd, f.OnMnuFilePebbleOpenClick) vcl_menuitem(mnuFilePebble, "New in-memory database", imgDatabaseAdd, f.OnMnuFilePebbleMemoryClick) // mnuFileRedis := vcl.NewMenuItem(mnuFile) mnuFileRedis.SetCaption("Redis") mnuFileRedis.SetImageIndex(imgVendorRedis) mnuFile.Add(mnuFileRedis) vcl_menuitem(mnuFileRedis, "Connect...", imgDatabaseAdd, f.OnMnuFileRedisConnectClick) // mnuFileSqlite := vcl.NewMenuItem(mnuFile) mnuFileSqlite.SetCaption("SQLite") mnuFileSqlite.SetImageIndex(imgVendorSqlite) mnuFile.Add(mnuFileSqlite) vcl_menuitem(mnuFileSqlite, "Open database...", imgDatabaseAdd, f.OnMnuFileSqliteOpenClick) vcl_menuitem(mnuFileSqlite, "New in-memory database", imgDatabaseAdd, f.OnMnuFileSqliteMemoryClick) vcl_menuseparator(mnuFileSqlite) f.SQLiteUseCliDriver = vcl_menuitem(mnuFileSqlite, "Connect using CLI driver (experimental)", -1, nil) f.SQLiteUseCliDriver.SetAutoCheck(true) // vcl_menuseparator(mnuFile) mnuFileExit := vcl.NewMenuItem(mnuFile) mnuFileExit.SetCaption("Exit") mnuFileExit.SetOnClick(f.OnMnuFileExitClick) mnuFile.Add(mnuFileExit) mnuQuery := vcl.NewMenuItem(f) mnuQuery.SetCaption("Query") mnuQueryExecute := vcl.NewMenuItem(mnuQuery) mnuQueryExecute.SetCaption("Execute") mnuQueryExecute.SetShortCutFromString("F5") mnuQueryExecute.SetOnClick(f.OnQueryExecute) mnuQueryExecute.SetImageIndex(imgLightning) mnuQuery.Add(mnuQueryExecute) mnuHelp := vcl.NewMenuItem(f) mnuHelp.SetCaption("Help") mnuHelpVersion := vcl.NewMenuItem(mnuHelp) mnuHelpVersion.SetCaption("Driver versions...") mnuHelpVersion.SetOnClick(f.OnMenuHelpVersion) mnuHelp.Add(mnuHelpVersion) mnuHelpHomepage := vcl.NewMenuItem(mnuHelp) mnuHelpHomepage.SetCaption("About " + APPNAME) mnuHelpHomepage.SetShortCutFromString("F1") mnuHelpHomepage.SetOnClick(f.OnMnuHelpHomepage) mnuHelp.Add(mnuHelpHomepage) f.Menu = vcl.NewMainMenu(f) f.Menu.SetImages(f.ImageList) f.Menu.Items().Add(mnuFile) f.Menu.Items().Add(mnuQuery) f.Menu.Items().Add(mnuHelp) // f.StatusBar = vcl.NewStatusBar(f) f.StatusBar.SetParent(f) f.StatusBar.SetSimpleText("") // f.Buckets = vcl.NewTreeView(f) f.Buckets.SetParent(f) f.Buckets.SetImages(f.ImageList) 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) f.Buckets.SetOnContextPopup(f.OnNavContextPopup) 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) // fill remaining space f.Tabs.SetImages(f.ImageList) propertiesTab := vcl.NewTabSheet(f.Tabs) propertiesTab.SetParent(f.Tabs) propertiesTab.SetCaption("Properties") propertiesTab.SetImageIndex(imgChartBar) 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(true) // Need to leave it enabled so scrolling works f.propertiesBox.SetColor(vcl_default_tab_background()) f.propertiesBox.SetBorderStyle(types.BsNone) f.propertiesBox.SetScrollBars(types.SsAutoVertical) dataTab := vcl.NewTabSheet(f.Tabs) dataTab.SetParent(f.Tabs) dataTab.SetCaption("Data") dataTab.SetImageIndex(imgTable) 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) f.contentBox.Columns().Clear() queryTab := vcl.NewTabSheet(f.Tabs) queryTab.SetParent(f.Tabs) queryTab.SetCaption("Query") queryTab.SetImageIndex(imgLightning) queryButtonBar := vcl.NewToolBar(queryTab) queryButtonBar.SetParent(queryTab) queryButtonBar.SetAlign(types.AlTop) queryButtonBar.BorderSpacing().SetLeft(MY_SPACING) queryButtonBar.BorderSpacing().SetTop(MY_SPACING) //queryButtonBar.BorderSpacing().SetBottom(1) queryButtonBar.BorderSpacing().SetRight(MY_SPACING) queryButtonBar.SetEdgeBorders(0) queryButtonBar.SetImages(f.ImageList) queryButtonBar.SetShowCaptions(true) queryExecBtn := vcl.NewToolButton(queryButtonBar) queryExecBtn.SetParent(queryButtonBar) queryExecBtn.SetCaption("Execute") // queryExecBtn.SetImageIndex(imgLightning) queryExecBtn.SetOnClick(f.OnQueryExecute) f.queryInput = vcl.NewMemo(queryTab) f.queryInput.SetParent(queryTab) f.queryInput.SetHeight(MY_HEIGHT) f.queryInput.SetAlign(types.AlTop) f.queryInput.SetTop(1) if runtime.GOOS == "windows" { f.queryInput.Font().SetName("Consolas") } else { f.queryInput.Font().SetName("monospace") } f.queryInput.BorderSpacing().SetLeft(MY_SPACING) //f.queryInput.BorderSpacing().SetTop(1) f.queryInput.BorderSpacing().SetRight(MY_SPACING) f.queryInput.SetBorderStyle(types.BsFrame) vsplit := vcl.NewSplitter(queryTab) vsplit.SetParent(queryTab) vsplit.SetAlign(types.AlTop) vsplit.SetTop(2) f.queryResult = vcl.NewListView(queryTab) f.queryResult.SetParent(queryTab) f.queryResult.SetAlign(types.AlClient) // fill remaining space f.queryResult.SetViewStyle(types.VsReport) // "Report style" i.e. has columns f.queryResult.SetAutoWidthLastColumn(true) f.queryResult.SetReadOnly(true) f.queryResult.Columns().Clear() f.queryResult.BorderSpacing().SetLeft(MY_SPACING) f.queryResult.BorderSpacing().SetRight(MY_SPACING) f.queryResult.BorderSpacing().SetBottom(MY_SPACING) f.none = &noLoadedDatabase{} f.OnNavChange(f, nil) // calls f.none.RenderForNav and sets up status bar content } func (f *TMainForm) OnMnuFileBoltNewClick(sender vcl.IObject) { dlg := vcl.NewSaveDialog(f) dlg.SetTitle("Save database as...") dlg.SetFilter("Bolt database|*.db|All files|*.*") ret := dlg.Execute() // Fake blocking if ret { f.boltAddDatabaseFromFile(dlg.FileName(), false) } } func (f *TMainForm) OnMnuFileBoltOpenClick(sender vcl.IObject) { dlg := vcl.NewOpenDialog(f) dlg.SetTitle("Select a database file...") dlg.SetFilter("Bolt database|*.db|All files|*.*") ret := dlg.Execute() // Fake blocking if ret { f.boltAddDatabaseFromFile(dlg.FileName(), false) } } func (f *TMainForm) OnMnuFileBoltOpenReadonlyClick(sender vcl.IObject) { dlg := vcl.NewOpenDialog(f) dlg.SetTitle("Select a database file...") dlg.SetFilter("Bolt database|*.db|All files|*.*") ret := dlg.Execute() // Fake blocking if ret { f.boltAddDatabaseFromFile(dlg.FileName(), true) } } func (f *TMainForm) OnMnuFileSqliteOpenClick(sender vcl.IObject) { cliDriver := f.SQLiteUseCliDriver.Checked() dlg := vcl.NewOpenDialog(f) dlg.SetTitle("Select a database file...") dlg.SetFilter("SQLite database|*.db;*.db3;*.sqlite;*.sqlite3|All files|*.*") ret := dlg.Execute() // Fake blocking if ret { f.sqliteAddDatabaseFromFile(dlg.FileName(), cliDriver) } } func (f *TMainForm) OnMnuFileBadgerOpenClick(sender vcl.IObject) { dlg := vcl.NewSelectDirectoryDialog(f) dlg.SetTitle("Select a database directory...") ret := dlg.Execute() // Fake blocking if ret { f.badgerAddDatabaseFromDirectory(dlg.FileName()) } } func (f *TMainForm) OnMnuFileBadgerMemoryClick(sender vcl.IObject) { f.badgerAddDatabaseFromMemory() } func (f *TMainForm) OnMnuFilePebbleOpenClick(sender vcl.IObject) { dlg := vcl.NewSelectDirectoryDialog(f) dlg.SetTitle("Select a database directory...") ret := dlg.Execute() // Fake blocking if ret { f.pebbleAddDatabaseFrom(dlg.FileName(), nil) } } func (f *TMainForm) OnMnuFileDebianOpenClick(sender vcl.IObject) { dlg := vcl.NewOpenDialog(f) dlg.SetTitle("Select a database file...") dlg.SetFilter("Debconf database|*.dat|All files|*.*") if runtime.GOOS == "linux" { dlg.SetInitialDir(`/var/cache/debconf/`) } ret := dlg.Execute() // Fake blocking if ret { f.debconfAddDatabaseFrom(dlg.FileName()) } } func (f *TMainForm) OnMnuFilePebbleMemoryClick(sender vcl.IObject) { f.pebbleAddDatabaseFromMemory() } func (f *TMainForm) OnMnuFileSqliteMemoryClick(sender vcl.IObject) { cliDriver := f.SQLiteUseCliDriver.Checked() f.sqliteAddDatabaseFromFile(`:memory:`, cliDriver) } func (f *TMainForm) OnMnuFileRedisConnectClick(sender vcl.IObject) { var child *TRedisConnectionDialog vcl.Application.CreateForm(&child) defer child.Free() var ret TRedisConnectionDialogResult child.CustomReturn = &ret mr := child.ShowModal() // Fake blocking if mr != types.MrOk || ret == nil { return // Cancelled } // Connect f.redisConnect(ret) } func (f *TMainForm) OnMnuFileExitClick(sender vcl.IObject) { f.Close() } func (f *TMainForm) OnMnuHelpHomepage(sender vcl.IObject) { err := browser.OpenURL(HOMEPAGE_URL) if err != nil { vcl.ShowMessage("Opening browser: " + err.Error()) } } func (f *TMainForm) OnMenuHelpVersion(sender vcl.IObject) { bi, ok := debug.ReadBuildInfo() if !ok { return } info := "This version of " + APPNAME + " was compiled with:\n\n" for _, dep := range bi.Deps { // Filter to only interesting things switch dep.Path { case `github.com/cockroachdb/pebble`, `github.com/dgraph-io/badger/v4`, `github.com/mattn/go-sqlite3`, `github.com/redis/go-redis/v9`, `go.etcd.io/bbolt`, `modernc.org/sqlite`: info += fmt.Sprintf("- %s %s\n", dep.Path, dep.Version) } } vcl.ShowMessage(info) } func (f *TMainForm) OnNavContextPopup(sender vcl.IObject, mousePos types.TPoint, handled *bool) { *handled = true curItem := f.Buckets.Selected() if curItem == nil { // Nothing is selected at all return } ndata := (*navData)(curItem.Data()) mnu := vcl.NewPopupMenu(f.Buckets) mnu.SetImages(f.ImageList) mnuRefresh := vcl.NewMenuItem(mnu) mnuRefresh.SetCaption("Refresh") mnuRefresh.SetImageIndex(imgArrowRefresh) mnuRefresh.SetOnClick(func(sender vcl.IObject) { f.OnNavContextRefresh(curItem, ndata) }) mnu.Items().Add(mnuRefresh) // Check what custom actions the ndata->db itself wants to add actions, err := ndata.ld.NavContext(ndata) if err != nil { vcl.ShowMessage(err.Error()) return } if len(actions) > 0 { mnuSep := vcl.NewMenuItem(mnu) mnuSep.SetCaption("-") mnu.Items().Add(mnuSep) for _, action := range actions { mnuAction := vcl.NewMenuItem(mnu) mnuAction.SetCaption(action.Name) cb := action.Callback // Copy to avoid reuse of loop variable mnuAction.SetOnClick(func(sender vcl.IObject) { cb(ndata) f.OnNavContextRefresh(curItem, ndata) }) mnu.Items().Add(mnuAction) } } if curItem.Parent() == nil { // Top-level item (database connection). Allow closing by right-click. mnuSep := vcl.NewMenuItem(mnu) mnuSep.SetCaption("-") mnu.Items().Add(mnuSep) mnuClose := vcl.NewMenuItem(mnu) mnuClose.SetCaption("Close") mnuClose.SetOnClick(f.OnNavContextClose) mnuClose.SetImageIndex(imgDatabaseDelete) mnu.Items().Add(mnuClose) } // Show popup mnu.Popup2() } func (f *TMainForm) OnNavContextRefresh(item *vcl.TTreeNode, ndata *navData) { isExpanded := item.Expanded() // Reset nav node to 'unloaded' state item.Collapse(true) item.DeleteChildren() item.SetHasChildren(true) ndata.childrenLoaded = false // Trigger a virtual reload item.Expand(false) // Calls OnNavExpanding to dynamically detect children // Restore previous gui state item.SetExpanded(isExpanded) } func (f *TMainForm) OnNavContextClose(sender vcl.IObject) { curItem := f.Buckets.Selected() if curItem == nil { return // Nothing selected (shouldn't happen) } if curItem.Parent() != nil { return // Selection is not top-level DB connection (shouldn't happen) } ndata := (*navData)(curItem.Data()) ndata.ld.Close() curItem.Delete() // n.b. This triggers OnNavChange, which will then re-render from noLoadedDatabase{} } func (f *TMainForm) OnQueryExecute(sender vcl.IObject) { // If query tab is not selected, switch to it, but do not exec if f.Tabs.ActivePageIndex() != 2 { f.Tabs.SetActivePageIndex(2) return } queryString := f.queryInput.Text() if f.queryInput.SelLength() > 0 { queryString = f.queryInput.SelText() // Just the selected text } // Execute node := f.Buckets.Selected() if node == nil { vcl.ShowMessage("No database selected") return } ndata := (*navData)(node.Data()) ndata.ld.ExecQuery(queryString, f.queryResult) } func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) { var ld loadedDatabase = f.none var ndata *navData = nil if node != nil && node.Data() != nil { ndata = (*navData)(node.Data()) ld = ndata.ld } ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function // We're in charge of common status bar text updates f.StatusBar.SetSimpleText(ld.DisplayName() + " | " + ld.DriverName()) } 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()) err := f.NavLoadChildren(node, ndata) if err != nil { if errors.Is(err, ErrNavNotExist) { // The nav entry has been deleted. // This is a normal thing to happen when e.g. deleting a table // f.StatusBar.SetSimpleText(err.Error()) // Just gets overridden when the selection changes node.Delete() return } vcl.ShowMessage(err.Error()) *allowExpansion = false // Permanently block return } *allowExpansion = node.HasChildren() // While we're here - preload one single level deep (not any deeper) if node.HasChildren() { // This node has children that we haven't processed. Process them now cc := node.GetFirstChild() for { ndataChild := (*navData)(cc.Data()) if ndataChild.childrenLoaded { break // We always do them together, so if one's done, no need to keep looking } f.NavLoadChildren(cc, ndataChild) cc = cc.GetNextSibling() if cc == nil { break } } } } func (f *TMainForm) NavLoadChildren(node *vcl.TTreeNode, ndata *navData) error { if ndata.childrenLoaded { return nil // Nothing to do } // Find the child buckets from this point under the element nextBucketNames, err := ndata.ld.NavChildren(ndata) if err != nil { return fmt.Errorf("Failed to find child buckets under %q: %w", strings.Join(ndata.bucketPath, `/`), err) } ndata.childrenLoaded = true // don't repeat this work if len(nextBucketNames) == 0 { node.SetHasChildren(false) } else { node.SetHasChildren(true) // Populate LCL child nodes for _, bucketName := range nextBucketNames { node := f.Buckets.Items().AddChild(node, formatUtf8([]byte(bucketName))) node.SetHasChildren(true) // dynamically populate in OnNavExpanding node.SetImageIndex(imgTable) node.SetSelectedIndex(imgTable) 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)) ndata.ld.Keepalive(navData) } } return nil }