54 Commits

Author SHA1 Message Date
eb34221620 doc/README: changelog for v0.4.0 2024-06-23 16:35:25 +12:00
cc3ba4d9f0 doc/TODO: update current progress 2024-06-23 16:29:53 +12:00
f79d17afed badger: support temporary in-memory databases 2024-06-23 16:29:53 +12:00
d7c2282335 badger: add dgraph vendor icon 2024-06-23 16:29:53 +12:00
43002a9fde main: restructure nav menu into alphabetical per-DB options 2024-06-23 16:29:53 +12:00
bc33d26cfd images: add vendor logos 2024-06-23 16:29:47 +12:00
38d9e6238f main: add help menu with website link 2024-06-23 15:58:12 +12:00
a653ef8ca4 main: get accurate cross-platform background colour for properties tab 2024-06-23 15:58:03 +12:00
cc336366c9 redis: show results in query tab 2024-06-23 15:57:33 +12:00
8f105183eb redis: handle data browsing for different typed keys 2024-06-23 15:57:20 +12:00
b45faa2e73 main: add nav context menu, support closing open connections 2024-06-23 15:28:15 +12:00
7e5d17100d main: preload recursive navigation only one layer at a time 2024-06-23 15:21:41 +12:00
a817e5fa21 main: recursively load all nav state at connection time 2024-06-23 15:12:26 +12:00
3d185033f3 main: allow scrolling long content on properties tab 2024-06-23 14:56:00 +12:00
924957d00d main: abstract the nil selection into a virtual database type 2024-06-23 14:55:46 +12:00
d674078071 main: auto switch to newly opened database 2024-06-23 14:54:49 +12:00
5992d19906 redis: complete basic multidatabase browse support 2024-06-23 14:54:33 +12:00
8cac46e9f2 doc: update README + TODO 2024-06-23 13:13:43 +12:00
db5f6816c5 main: allow running partial query by selection 2024-06-23 13:07:52 +12:00
3a4bdbde94 redis: improve dialog style, open connection, enumerate databases 2024-06-23 13:07:42 +12:00
0363bc65f4 platform_windows: move extra syso inclusion out of main 2024-06-23 13:06:41 +12:00
a47898e099 format: move to util_ file 2024-06-23 13:06:25 +12:00
1487b18a3a doc/TODO: add notes re virtual list rendering 2024-06-22 17:36:34 +12:00
04ef53720f redis: initial work on connection dialog 2024-06-22 17:36:25 +12:00
bc82aacd57 redis: add library dependency 2024-06-22 17:36:19 +12:00
6dcc1afd6b rename files to db_ prefixes 2024-06-22 16:22:33 +12:00
645ab29cdd doc/README: changelog for v0.3.0 2024-06-15 13:22:02 +12:00
4202b9b970 doc/README: update current features 2024-06-15 13:21:55 +12:00
42bbe3957a gitignore: exclude test data folder 2024-06-15 13:17:59 +12:00
5e2aae9032 sqlite: use cgo-free driver if necessary 2024-06-15 13:16:48 +12:00
f54577f93f doc/TODO: initial commit 2024-06-15 13:08:14 +12:00
f4d2d2ec39 gui: improvements for execute toolbar 2024-06-15 12:57:35 +12:00
5cd3f6c765 badger: initial support 2024-06-15 12:37:44 +12:00
5d268d22af gui: strip extra quote marks from string cells 2024-06-15 12:15:29 +12:00
5e0422e10f gui: add query shortcut, or switch tab if not focused 2024-06-15 12:15:29 +12:00
ef30a0d210 add new custom query feature 2024-06-15 12:15:21 +12:00
38847b3a7e sqlite: refactor separate populateRows, populateColumns 2024-06-15 12:13:33 +12:00
f432b52652 gui: use soft close from menuitem instead of hard pkill 2024-06-15 11:44:19 +12:00
f64522dfa1 gui: more icons on tables, tabs 2024-06-15 11:43:58 +12:00
79bce40581 gui: fix icons going missing when selecting in nav tree 2024-06-15 11:43:36 +12:00
3731ee1781 show current/selected driver name in status bar 2024-06-15 11:43:24 +12:00
2481a1da20 gui: set main app icon 2024-06-15 11:42:23 +12:00
1035086ed4 images: add more images 2024-06-15 11:42:13 +12:00
038eb44f48 bolt: update v1.3.10 to v1.4.0-alpha.1 2024-06-15 11:42:02 +12:00
841575700e doc/README: fill in current statuses 2024-06-08 15:09:26 +12:00
617393b627 sqlite: load table data 2024-06-08 15:02:02 +12:00
91f9c5fc30 sqlite: load column headers 2024-06-08 14:49:57 +12:00
6234f02ea6 gui: attach some icons to menu and nav tree 2024-06-08 14:24:44 +12:00
8b1e7064e7 gui: create a TImageList with famfamfam/silk resources 2024-06-08 14:24:35 +12:00
00a96bfe84 sqlite: pull table schema 2024-06-08 14:23:38 +12:00
232a1dd0e8 sqlite: initial support 2024-06-08 13:44:18 +12:00
cb4b35b059 gui: add exit menu item 2024-06-08 13:34:49 +12:00
f913b63c58 bolt: refactor extract to separate interface 2024-06-08 13:34:33 +12:00
d97c8872de doc: add v0.1.0 changelog 2024-06-08 13:33:54 +12:00
36 changed files with 1921 additions and 201 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
testdata/
liblcl*
yvbolt
yvbolt.exe

View File

@@ -1,21 +1,70 @@
# yvbolt
A graphical browser for [Bolt databases](https://github.com/etcd-io/bbolt) using [GoVCL](https://z-kit.cc/en/).
A graphical browser for multiple databases using [GoVCL](https://z-kit.cc/en/).
This is an experimental application and you should generally prefer to use [qbolt](https://code.ivysaur.me/qbolt).
## Features
- Supports Bolt databases using the upstream etcd.io/bbolt library
- Browse database content
- Recursive bucket support
- Safe handling for non-UTF8 key and data fields
- No CGO for easy cross-compilation
- Permissive ISC license
- Native desktop application, running on Linux, Windows, and macOS
- Connect to multiple databases
- Browse table/bucket content
- Run custom SQL queries
- Select text to run partial query
- Safe handling for non-UTF8 key and data fields
- Supported databases:
- Badger v4
- Bolt
- Full compatibility via the upstream [etcd-io/bbolt](https://github.com/etcd-io/bbolt) library
- Recursive bucket support
- SQLite
- Uses CGo if available or modernc.org if not
- Redis
## License
The code in this project is licensed under the ISC license (see `LICENSE` file for details).
This project redistributes images from the famfamfam/silk icon set under the [CC-BY 2.5 license](http://creativecommons.org/licenses/by/2.5/).
This project includes trademarked logo images for each supported database type.
## Usage
1. `go build`
1. `CGO_ENABLED=1 go build`
2. [Download liblcl](https://github.com/ying32/govcl/releases/download/v2.2.3/liblcl-2.2.3.zip) for your platform, or [compile it yourself](https://github.com/ying32/liblcl) (tested with v2.2.3)
3. Place the liblcl library file in the same directory as `yvbolt`
4. Run `yvbolt` and use the main menu to open a database
## Changelog
2024-06-23 v0.4.0
- Redis: Add as supported database
- Badger: Allow creating in-memory databases
- App: Allow selecting partial query text to execute
- App: Allow closing database connections from context menu
- App: Allow scrolling large content on Properties pane
- App: Preload recursive navigation
- App: Automatically switch to selected database when new connection is created
- App: Add help website link
- App: Add database logo images
2024-06-25 v0.3.0
- Badger: Add BadgerDB v4 as supported database
- SQLite: Add support for CGo-free SQLite driver under cross-compilation
- Bolt: Update Bolt to v1.4.0-alpha.1
- App: Add support for running custom queries
- App: Add status bar showing currently selected DB
- App: Fix missing icons in nav when selecting items
- App: Fix extra quotemarks when browsing string content of database
2024-06-08 v0.2.0
- SQLite: Add SQLite support (now requires CGo)
- App: Add images for menu and navigation items
2024-06-03 v0.1.0
- Initial public release

24
TODO Normal file
View File

@@ -0,0 +1,24 @@
- Insert
- Update cell
- Delete row(s)
- Binary data viewer
- Detect jpg/png and show as image
- Warning if data table is filtered to 1000 rows, or add pagination
- More DB types
- MySQL
- Postgres
- MSSQL (recursive navigation for instances)
- SSH tunnels
- Special actions
- Refresh tables
- Per-db actions e.g. compact, export backup
- Per-table actions e.g. drop table
- Sqlite cli driver for ssh tunnel
- https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/
- https://github.com/litements/litexplore
- Badger encryption key dialog
- Makefile to cross-compile release binaries in docker
- Build own liblcl binaries in docker
- Win32 icon resource
- Faster virtual rendering
- https://github.com/ying32/govcl/blob/master/samples/listviewvirtualdata/main.go

BIN
assets/chart_bar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

BIN
assets/database.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

BIN
assets/database_add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

BIN
assets/database_delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

BIN
assets/database_save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

BIN
assets/lightning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

BIN
assets/table.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

BIN
assets/table_add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

BIN
assets/table_delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

BIN
assets/table_save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

BIN
assets/vendor_dgraph.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

BIN
assets/vendor_github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

BIN
assets/vendor_mysql.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

BIN
assets/vendor_redis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/vendor_sqlite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 B

160
db_badger.go Normal file
View File

@@ -0,0 +1,160 @@
package main
import (
"fmt"
"path/filepath"
"unsafe"
"github.com/dgraph-io/badger/v4"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
)
type badgerLoadedDatabase struct {
displayName string
db *badger.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *badgerLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *badgerLoadedDatabase) DriverName() string {
return "Badger v4"
}
func (ld *badgerLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *badgerLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *badgerLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
// Load properties
content := fmt.Sprintf("Table statistics: %#v", ld.db.Tables())
f.propertiesBox.SetText(content)
// Load data
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
// Badger always uses Key + Value as the columns
f.contentBox.Columns().Clear()
colKey := f.contentBox.Columns().Add()
colKey.SetCaption("Key")
colKey.SetWidth(MY_WIDTH)
colKey.SetAlignment(types.TaLeftJustify)
colVal := f.contentBox.Columns().Add()
colVal.SetCaption("Value")
err := ld.db.View(func(txn *badger.Txn) error {
// Valid
f.contentBox.Clear()
// Create iterator
opts := badger.DefaultIteratorOptions
opts.PrefetchSize = 64
it := txn.NewIterator(opts)
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
item := it.Item()
k := item.Key()
err := item.Value(func(v []byte) error {
dataEntry := f.contentBox.Items().Add()
dataEntry.SetCaption(formatUtf8(k))
dataEntry.SubItems().Add(formatUtf8(v))
return nil
})
if err != nil {
return err
}
}
return nil
})
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load data: %s", err.Error()))
return
}
// Valid
f.contentBox.SetEnabled(true)
}
func (ld *badgerLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// In the Badger implementation, there is only one child: "Data"
if len(ndata.bucketPath) == 0 {
return []string{"Data"}, nil
} else {
// No children deeper than that
return []string{}, nil
}
}
func (ld *badgerLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
vcl.ShowMessage("Badger doesn't support querying")
}
func (ld *badgerLoadedDatabase) Close() {
_ = ld.db.Close()
ld.arena = nil
}
var _ loadedDatabase = &badgerLoadedDatabase{} // interface assertion
//
func (f *TMainForm) badgerAddDatabaseFromMemory() {
f.badgerAddDatabaseFrom(badger.DefaultOptions("").WithInMemory(true))
}
func (f *TMainForm) badgerAddDatabaseFromDirectory(path string) {
f.badgerAddDatabaseFrom(badger.DefaultOptions(path))
}
func (f *TMainForm) badgerAddDatabaseFrom(opts badger.Options) {
// TODO load in background thread to stop blocking the UI
db, err := badger.Open(opts)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database: %s", err.Error()))
return
}
ld := &badgerLoadedDatabase{
db: db,
}
if opts.Dir == "" {
ld.displayName = ":memory:" // SQLite-style naming
} else {
ld.displayName = filepath.Base(opts.Dir)
}
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
}
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
f.Buckets.SetSelected(ld.nav) // Select new element
ld.Keepalive(navData)
}

206
db_bolt.go Normal file
View File

@@ -0,0 +1,206 @@
package main
import (
"fmt"
"path/filepath"
"sort"
"strings"
"time"
"unsafe"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
"go.etcd.io/bbolt"
"go.etcd.io/bbolt/version"
)
type boltLoadedDatabase struct {
displayName string
path string
db *bbolt.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *boltLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *boltLoadedDatabase) DriverName() string {
return "Bolt " + version.Version
}
func (ld *boltLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *boltLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
// Load properties
bucketDisplayName := strings.Join(ndata.bucketPath, `/`)
content := fmt.Sprintf("Selected database: %#v\n\n\nSelected bucket: %q\n", ld.db.Stats(), bucketDisplayName)
f.propertiesBox.SetText(content)
// Load data
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
// Bolt always uses Key + Value as the columns
f.contentBox.Columns().Clear()
colKey := f.contentBox.Columns().Add()
colKey.SetCaption("Key")
colKey.SetWidth(MY_WIDTH)
colKey.SetAlignment(types.TaLeftJustify)
colVal := f.contentBox.Columns().Add()
colVal.SetCaption("Value")
err := 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 (ld *boltLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// In the bolt implementation, the nav is a recursive tree of child buckets
return boltChildBucketNames(ld.db, ndata.bucketPath)
}
func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
vcl.ShowMessage("Bolt doesn't support querying")
}
func (ld *boltLoadedDatabase) Close() {
_ = ld.db.Close()
ld.arena = nil
}
var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion
//
func (f *TMainForm) boltAddDatabaseFromFile(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
}
ld := &boltLoadedDatabase{
path: path,
displayName: filepath.Base(path),
db: db,
}
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
}
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
f.Buckets.SetSelected(ld.nav) // Select new element
ld.Keepalive(navData)
}
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
}

37
db_none.go Normal file
View File

@@ -0,0 +1,37 @@
package main
import (
"github.com/ying32/govcl/vcl"
)
type noLoadedDatabase struct{}
func (n *noLoadedDatabase) DisplayName() string {
return "yvbolt"
}
func (n *noLoadedDatabase) DriverName() string {
return "No database selected"
}
func (n *noLoadedDatabase) RootElement() *vcl.TTreeNode {
return nil
}
func (n *noLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
f.propertiesBox.SetText("Open a database to get started...")
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
}
func (n *noLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
}
func (n *noLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
return []string{}, nil
}
func (n *noLoadedDatabase) Keepalive(ndata *navData) {
}
func (n *noLoadedDatabase) Close() {}

282
db_redis.go Normal file
View File

@@ -0,0 +1,282 @@
package main
import (
"context"
"fmt"
"strconv"
"strings"
"unsafe"
"github.com/redis/go-redis/v9"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
)
type redisLoadedDatabase struct {
displayName string
db *redis.Client
nav *vcl.TTreeNode
currentDb int
maxDb int
serverVersion string // populated at connection-time from INFO command
arena []*navData // keepalive
}
func (ld *redisLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *redisLoadedDatabase) DriverName() string {
return "Redis " + ld.serverVersion
}
func (ld *redisLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *redisLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
ctx := context.Background()
if len(ndata.bucketPath) == 0 {
// Top-level: Show info() on main Properties tab
infostr, err := ld.db.Info(ctx).Result()
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Retreiving database info: %v", err))
return
}
f.propertiesBox.SetText(infostr)
// Disable data tab
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
} else if len(ndata.bucketPath) == 1 {
// One selected database
// Figure out its content
err := ld.db.Do(ctx, "SELECT", ndata.bucketPath[0]).Err()
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Switching to database %q: %v", ndata.bucketPath[0], err))
return
}
allKeys, err := ld.db.Keys(ctx, "*").Result()
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Listing keys in database %q: %v", ndata.bucketPath[0], err))
return
}
f.propertiesBox.SetText(fmt.Sprintf("Database %s\nTotal keys: %d\n", ndata.bucketPath[0], len(allKeys)))
// Redis always uses Key + Value as the columns
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
f.contentBox.Columns().Clear()
colKey := f.contentBox.Columns().Add()
colKey.SetCaption("Key")
colKey.SetWidth(MY_WIDTH)
colKey.SetAlignment(types.TaLeftJustify)
colType := f.contentBox.Columns().Add()
colType.SetCaption("Type")
colVal := f.contentBox.Columns().Add()
colVal.SetCaption("Value")
for _, key := range allKeys {
typeName, err := ld.db.Type(ctx, key).Result()
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
return
}
dataEntry := f.contentBox.Items().Add()
dataEntry.SetCaption(key) // formatUtf8
dataEntry.SubItems().Add(typeName)
switch typeName {
case "string":
val, err := ld.db.Get(ctx, key).Result()
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
return
}
dataEntry.SubItems().Add(val)
case "hash":
val, err := ld.db.HGetAll(ctx, key).Result()
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
return
}
// It's a map[string]string
dataEntry.SubItems().Add(formatAny(val))
case "lists":
fallthrough
case "sets":
fallthrough
case "sorted sets":
fallthrough
case "stream":
fallthrough
default:
dataEntry.SubItems().Add("<<<other object type>>>")
}
}
// Valid
f.contentBox.SetEnabled(true)
} else {
vcl.ShowMessage(fmt.Sprintf("Unexpected nav position %q", ndata.bucketPath))
return
}
}
func (ld *redisLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// ctx := context.Background()
if len(ndata.bucketPath) == 0 {
// Top-level: list of all child databases (usually 16x)
ret := make([]string, 0, ld.maxDb)
for i := 0; i < ld.maxDb; i++ {
ret = append(ret, fmt.Sprintf("%d", i))
}
return ret, nil
} else if len(ndata.bucketPath) == 1 {
// One selected database
// No child keys underneath it
return []string{}, nil
} else {
return nil, fmt.Errorf("Unexpected nav position %q", ndata.bucketPath)
}
}
func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
ctx := context.Background()
// Need to parse the query into separate string+args fields for the protocol
// TODO This needs to better handle quotes, escaping, ...
fields := strings.Fields(query)
fields_boxed := box_interface(fields)
ret, err := ld.db.Do(ctx, fields_boxed...).Result()
if err != nil {
vcl.ShowMessage(fmt.Sprintf("The redis query returned an error: %v", err))
return
}
resultArea.SetEnabled(false)
resultArea.Clear()
resultArea.Columns().Clear()
colVal := resultArea.Columns().Add()
colVal.SetCaption("Result")
// The result is probably a single value or a string slice
switch ret := ret.(type) {
case []string:
// Multiple values
for _, single := range ret {
cell := resultArea.Items().Add()
cell.SetCaption(single) // formatUtf8
}
default:
// Single value
dataEntry := resultArea.Items().Add()
dataEntry.SetCaption(formatAny(ret)) // formatUtf8
}
resultArea.SetEnabled(true)
}
func (ld *redisLoadedDatabase) Close() {
_ = ld.db.Close()
ld.arena = nil
}
var _ loadedDatabase = &redisLoadedDatabase{} // interface assertion
//
func (f *TMainForm) redisConnect(opts *redis.Options) {
// TODO load in background thread to stop blocking the UI
ctx := context.Background()
ld := &redisLoadedDatabase{
displayName: opts.Addr,
currentDb: 0,
maxDb: 1,
serverVersion: "<unknown>",
}
// Patch in a hook to remember current DB after keepalive reconnection
opts.DB = 0 // Default
opts.OnConnect = func(ctx context.Context, cn *redis.Conn) error {
return cn.Select(ctx, ld.currentDb).Err()
}
// NewClient doesn't necessarily connect, so it can't throw an err
ld.db = redis.NewClient(opts)
// Make an INFO request (mandatory)
info, err := ld.db.InfoMap(ctx).Result()
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to connect to Redis server: During INFO: %v", err))
return
}
if serverInfo, ok := info["Server"]; ok {
if v, ok := serverInfo["redis_version"]; ok {
ld.serverVersion = v
}
}
// List available databases (usually 1..16) with "0" as default
// If this fails, probably the target redis does not support multiple databases
// (e.g. Redis Cluster). Assume max=0.
if maxDatabases, err := ld.db.ConfigGet(ctx, "databases").Result(); err == nil {
// Got a result. Must parse it
m, err := strconv.ParseInt(maxDatabases["databases"], 10, 64)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to connect to Redis server: During CONFIG GET databases: %v", err))
return
}
ld.maxDb = int(m)
}
// Done with setup
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
}
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
f.Buckets.SetSelected(ld.nav) // Select new element
ld.Keepalive(navData)
}

272
db_sqlite.go Normal file
View File

@@ -0,0 +1,272 @@
package main
import (
"database/sql"
"fmt"
"path/filepath"
"unsafe"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
)
const (
sqliteTablesCaption = "Tables"
)
type sqliteLoadedDatabase struct {
displayName string
path string
db *sql.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *sqliteLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *sqliteLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *sqliteLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
if len(ndata.bucketPath) == 0 {
// Top-level
f.propertiesBox.SetText("Please select...")
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
} else if len(ndata.bucketPath) == 1 {
// Category (tables, ...)
f.propertiesBox.SetText("Please select...")
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
} else if len(ndata.bucketPath) == 2 && ndata.bucketPath[0] == sqliteTablesCaption {
// Render for specific table
tableName := ndata.bucketPath[1]
// Get some basic properties
r := ld.db.QueryRow(`SELECT sql FROM sqlite_schema WHERE name = ?;`, tableName)
var schemaStmt string
err := r.Scan(&schemaStmt)
if err != nil {
schemaStmt = fmt.Sprintf("* Failed to describe table %q: %s", tableName, err.Error())
}
// Display table properties
f.propertiesBox.SetText(fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt))
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
// Load column details
// Use SELECT form instead of common PRAGMA table_info so we can just get names
// We could possibly get this from the main data select, but this will
// work even when there are 0 results
columnNames, err := ld.sqliteGetColumnNamesForTable(tableName)
if err != nil {
vcl.ShowMessageFmt("Failed to load columns for table %q: %s", tableName, err.Error())
return
}
populateColumns(columnNames, f.contentBox)
// Select count(*) so we know to display a warning if there are too many entries
// TODO
// Select * with small limit
datar, err := ld.db.Query(`SELECT * FROM ` + tableName + ` LIMIT 1000`) // WARNING can't prepare this parameter, but it comes from the DB (trusted)
if err != nil {
vcl.ShowMessageFmt("Failed to load data for table %q: %s", tableName, err.Error())
return
}
defer datar.Close()
populateRows(datar, f.contentBox)
// We successfully populated the data grid
f.contentBox.SetEnabled(true)
} else {
// ??? unknown
}
}
func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) ([]string, error) {
colr, err := ld.db.Query(`SELECT name FROM pragma_table_info(?)`, tableName)
if err != nil {
return nil, fmt.Errorf("Query: %w", err)
}
defer colr.Close()
var ret []string
for colr.Next() {
var columnName string
err = colr.Scan(&columnName)
if err != nil {
return nil, fmt.Errorf("Scan: %w", colr.Err())
}
ret = append(ret, columnName)
}
if colr.Err() != nil {
return nil, colr.Err()
}
return ret, nil
}
func populateColumns(names []string, dest *vcl.TListView) {
dest.Columns().Clear()
for _, columnName := range names {
col := dest.Columns().Add()
col.SetCaption(columnName)
col.SetWidth(MY_WIDTH)
col.SetAlignment(types.TaLeftJustify)
}
}
func populateRows(rr *sql.Rows, dest *vcl.TListView) {
numColumns := int(dest.Columns().Count())
for rr.Next() {
fields := make([]interface{}, numColumns)
pfields := make([]interface{}, numColumns)
for i := 0; i < numColumns; i += 1 {
pfields[i] = &fields[i]
}
err := rr.Scan(pfields...)
if err != nil {
vcl.ShowMessageFmt("Failed to load data: %s", err.Error())
return
}
dataEntry := dest.Items().Add()
dataEntry.SetCaption(formatAny(fields[0]))
for i := 1; i < len(fields); i += 1 {
dataEntry.SubItems().Add(formatAny(fields[i]))
}
}
if rr.Err() != nil {
vcl.ShowMessageFmt("Failed to load data: %s", rr.Err().Error())
return
}
}
func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
rr, err := ld.db.Query(query)
if err != nil {
vcl.ShowMessage(err.Error())
return
}
defer rr.Close()
resultArea.SetEnabled(false)
resultArea.Clear()
columns, err := rr.Columns()
if err != nil {
vcl.ShowMessage(err.Error())
return
}
populateColumns(columns, resultArea)
populateRows(rr, resultArea)
resultArea.SetEnabled(true)
}
func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
if len(ndata.bucketPath) == 0 {
// The top-level children are always:
return []string{sqliteTablesCaption}, nil
}
if len(ndata.bucketPath) == 1 && ndata.bucketPath[0] == sqliteTablesCaption {
rr, err := ld.db.Query(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name ASC;`)
if err != nil {
return nil, err
}
defer rr.Close()
var gather []string
for rr.Next() {
var tableName string
err = rr.Scan(&tableName)
if err != nil {
return nil, err
}
gather = append(gather, tableName)
}
if rr.Err() != nil {
return nil, rr.Err()
}
return gather, nil
}
if len(ndata.bucketPath) == 2 {
return nil, nil // Never any deeper children
}
return nil, fmt.Errorf("unknown nav path %#v", ndata.bucketPath)
}
func (ld *sqliteLoadedDatabase) Close() {
_ = ld.db.Close()
ld.arena = nil
}
var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion
//
func (f *TMainForm) sqliteAddDatabaseFromFile(path string) {
// TODO load in background thread to stop blocking the UI
db, err := sql.Open("sqlite3", path)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error()))
return
}
ld := &sqliteLoadedDatabase{
path: path,
displayName: filepath.Base(path),
db: db,
}
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
}
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
f.Buckets.SetSelected(ld.nav) // Select new element
ld.Keepalive(navData)
}

12
db_sqlite_cgo.go Normal file
View File

@@ -0,0 +1,12 @@
//+build cgo
package main
import (
sqlite3 "github.com/mattn/go-sqlite3"
)
func (ld *sqliteLoadedDatabase) DriverName() string {
ver1, _, _ := sqlite3.Version()
return "SQLite " + ver1
}

11
db_sqlite_nocgo.go Normal file
View File

@@ -0,0 +1,11 @@
//+build !cgo
package main
import (
_ "modernc.org/sqlite"
)
func (ld *sqliteLoadedDatabase) DriverName() string {
return "SQLite (modernc.org)"
}

45
go.mod
View File

@@ -3,7 +3,46 @@ module yvbolt
go 1.19
require (
github.com/ying32/govcl v2.2.3+incompatible // indirect
go.etcd.io/bbolt v1.3.10 // indirect
golang.org/x/sys v0.4.0 // indirect
github.com/dgraph-io/badger/v4 v4.2.0
github.com/mattn/go-sqlite3 v1.14.22
github.com/ying32/govcl v2.2.3+incompatible
go.etcd.io/bbolt v1.4.0-alpha.1
modernc.org/sqlite v1.24.0
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/google/flatbuffers v1.12.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.12.3 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/redis/go-redis/v9 v9.5.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
go.opencensus.io v0.22.5 // indirect
golang.org/x/mod v0.3.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/tools v0.0.0-20210106214847-113979e3529a // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
)

168
go.sum
View File

@@ -1,6 +1,166 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs=
github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/ying32/govcl v2.2.3+incompatible h1:Iyfcl26yNE1USm+3uG+btQyhkoFIV18+VITrUdHu8Lw=
github.com/ying32/govcl v2.2.3+incompatible/go.mod h1:yZVtbJ9Md1nAVxtHKIriKZn4K6TQYqI1en3sN/m9FJ8=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.4.0-alpha.1 h1:3yrqQzbRRPFPdOMWS/QQIVxVnzSkAZQYeWlZFv1kbj4=
go.etcd.io/bbolt v1.4.0-alpha.1/go.mod h1:S/Z/Nm3iuOnyO1W4XuFfPci51Gj6F1Hv0z8hisyYYOw=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI=
modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=

66
images.go Normal file
View File

@@ -0,0 +1,66 @@
package main
import (
"embed"
"github.com/ying32/govcl/vcl"
)
//go:embed assets/*
var assetsFs embed.FS
const (
imgChartBar int32 = iota
imgDatabase
imgDatabaseAdd
imgDatabaseDelete
imgDatabaseLightning
imgDatabaseSave
imgLightning
imgTable
imgTableAdd
imgTableDelete
imgTableSave
imgVendorDgraph
imgVendorGithub
imgVendorMySQL
imgVendorRedis
imgVendorSqlite
)
func loadImages(owner vcl.IComponent) *vcl.TImageList {
mustLoad := func(n string) *vcl.TBitmap {
imgData, err := assetsFs.ReadFile(n)
if err != nil {
panic(err)
}
png := vcl.NewPngImage()
png.LoadFromBytes(imgData)
ret := vcl.NewBitmap()
ret.Assign(png)
return ret
}
ilist := vcl.NewImageList(owner)
ilist.Add(mustLoad("assets/chart_bar.png"), nil)
ilist.Add(mustLoad("assets/database.png"), nil)
ilist.Add(mustLoad("assets/database_add.png"), nil)
ilist.Add(mustLoad("assets/database_delete.png"), nil)
ilist.Add(mustLoad("assets/database_lightning.png"), nil)
ilist.Add(mustLoad("assets/database_save.png"), nil)
ilist.Add(mustLoad("assets/lightning.png"), nil)
ilist.Add(mustLoad("assets/table.png"), nil)
ilist.Add(mustLoad("assets/table_add.png"), nil)
ilist.Add(mustLoad("assets/table_delete.png"), nil)
ilist.Add(mustLoad("assets/table_save.png"), nil)
ilist.Add(mustLoad("assets/vendor_dgraph.png"), nil)
ilist.Add(mustLoad("assets/vendor_github.png"), nil)
ilist.Add(mustLoad("assets/vendor_mysql.png"), nil)
ilist.Add(mustLoad("assets/vendor_redis.png"), nil)
ilist.Add(mustLoad("assets/vendor_sqlite.png"), nil)
return ilist
}

24
loadedDatabase.go Normal file
View File

@@ -0,0 +1,24 @@
package main
import (
"github.com/ying32/govcl/vcl"
)
// loadedDatabase is a DB-agnostic interface for each loaded database.
type loadedDatabase interface {
DisplayName() string
DriverName() string
RootElement() *vcl.TTreeNode
RenderForNav(f *TMainForm, ndata *navData)
ExecQuery(query string, resultArea *vcl.TListView)
NavChildren(ndata *navData) ([]string, error)
Keepalive(ndata *navData)
Close()
}
// navData is the .Data() pointer for each TTreeNode in the left-hand tree.
type navData struct {
ld loadedDatabase
childrenLoaded bool
bucketPath []string
}

549
main.go
View File

@@ -2,43 +2,30 @@ 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/pkg/browser"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
"go.etcd.io/bbolt"
"github.com/ying32/govcl/vcl/types/colors"
)
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
ImageList *vcl.TImageList
Menu *vcl.TMainMenu
StatusBar *vcl.TStatusBar
Buckets *vcl.TTreeView
Tabs *vcl.TPageControl
propertiesBox *vcl.TMemo
contentBox *vcl.TListView
queryInput *vcl.TMemo
queryResult *vcl.TListView
dbs []loadedDatabase
arena []*navData // keepalive
none *noLoadedDatabase
dbs []loadedDatabase
}
var (
@@ -50,30 +37,131 @@ func main() {
}
func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
const MY_SPACING = 6
const MY_WIDTH = 180
f.ImageList = loadImages(f)
f.SetCaption("yvbolt")
f.ScreenCenter()
f.ImageList.GetIcon(imgDatabaseLightning, f.Icon())
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)
mnuFileBadger := vcl.NewMenuItem(mnuFile)
mnuFileBadger.SetCaption("Badger")
mnuFileBadger.SetImageIndex(imgVendorDgraph)
mnuFile.Add(mnuFileBadger)
mnuFileBadgerOpen := vcl.NewMenuItem(mnuFileBadger)
mnuFileBadgerOpen.SetCaption("Open database...")
mnuFileBadgerOpen.SetImageIndex(imgDatabaseAdd)
mnuFileBadgerOpen.SetOnClick(f.OnMnuFileBadgerOpenClick)
mnuFileBadger.Add(mnuFileBadgerOpen)
mnuFileBadgerMemory := vcl.NewMenuItem(mnuFileBadger)
mnuFileBadgerMemory.SetCaption("New in-memory database")
mnuFileBadgerMemory.SetImageIndex(imgDatabaseAdd)
mnuFileBadgerMemory.SetOnClick(f.OnMnuFileBadgerMemoryClick)
mnuFileBadger.Add(mnuFileBadgerMemory)
//
mnuFileBolt := vcl.NewMenuItem(mnuFile)
mnuFileBolt.SetCaption("Bolt")
mnuFileBolt.SetImageIndex(imgVendorGithub)
mnuFile.Add(mnuFileBolt)
mnuFileBoltOpen := vcl.NewMenuItem(mnuFileBolt)
mnuFileBoltOpen.SetCaption("Open database...")
mnuFileBoltOpen.SetImageIndex(imgDatabaseAdd)
mnuFileBoltOpen.SetShortCutFromString("Ctrl+O")
mnuFileBoltOpen.SetOnClick(f.OnMnuFileOpenClick)
mnuFileBolt.Add(mnuFileBoltOpen)
//
mnuFileRedis := vcl.NewMenuItem(mnuFile)
mnuFileRedis.SetCaption("Redis")
mnuFileRedis.SetImageIndex(imgVendorRedis)
mnuFile.Add(mnuFileRedis)
mnuFileRedisConnect := vcl.NewMenuItem(mnuFileRedis)
mnuFileRedisConnect.SetCaption("Connect...")
mnuFileRedisConnect.SetImageIndex(imgDatabaseAdd)
mnuFileRedisConnect.SetOnClick(f.OnMnuFileRedisConnectClick)
mnuFileRedis.Add(mnuFileRedisConnect)
//
mnuFileSqlite := vcl.NewMenuItem(mnuFile)
mnuFileSqlite.SetCaption("SQLite")
mnuFileSqlite.SetImageIndex(imgVendorSqlite)
mnuFile.Add(mnuFileSqlite)
mnuFileSqliteOpen := vcl.NewMenuItem(mnuFileSqlite)
mnuFileSqliteOpen.SetCaption("Open database...")
mnuFileSqliteOpen.SetImageIndex(imgDatabaseAdd)
mnuFileSqliteOpen.SetOnClick(f.OnMnuFileSqliteOpenClick)
mnuFileSqlite.Add(mnuFileSqliteOpen)
mnuFileSqliteMemory := vcl.NewMenuItem(mnuFileSqlite)
mnuFileSqliteMemory.SetCaption("New in-memory database")
mnuFileSqliteMemory.SetImageIndex(imgDatabaseAdd)
mnuFileSqliteMemory.SetOnClick(f.OnMnuFileSqliteMemoryClick)
mnuFileSqlite.Add(mnuFileSqliteMemory)
//
mnuSep := vcl.NewMenuItem(mnuFile)
mnuSep.SetCaption("-") // Creates separator
mnuFile.Add(mnuSep)
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")
mnuHelpHomepage := vcl.NewMenuItem(mnuHelp)
mnuHelpHomepage.SetCaption("About yvbolt")
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)
@@ -82,24 +170,29 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
f.Tabs = vcl.NewPageControl(f)
f.Tabs.SetParent(f)
f.Tabs.SetAlign(types.AlClient)
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(false)
f.propertiesBox.SetEnabled(true) // Need to leave it enabled so scrolling works
f.propertiesBox.SetColor(colors.ClForm) // 0x00f0f0f0
f.propertiesBox.SetBorderStyle(types.BsNone)
f.propertiesBox.SetText("Open a database to get started...")
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)
@@ -108,145 +201,220 @@ func (f *TMainForm) OnFormCreate(sender vcl.IObject) {
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")
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(0)
queryButtonBar.BorderSpacing().SetRight(MY_SPACING)
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)
f.queryInput.Font().SetName("monospace")
f.queryInput.BorderSpacing().SetLeft(MY_SPACING)
f.queryInput.BorderSpacing().SetTop(0)
f.queryInput.BorderSpacing().SetRight(MY_SPACING)
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) OnMnuFileOpenClick(sender vcl.IObject) {
dlg := vcl.NewOpenDialog(f)
dlg.SetTitle("Select a database file...")
dlg.SetFilter("Bolt database|*.db|SQLite database|*.db3|All files|*.*")
dlg.SetFilter("Bolt database|*.db|All files|*.*")
ret := dlg.Execute() // Fake blocking
if ret {
f.addDatabaseFromFile(dlg.FileName())
f.boltAddDatabaseFromFile(dlg.FileName())
}
}
func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) {
func (f *TMainForm) OnMnuFileSqliteOpenClick(sender vcl.IObject) {
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())
}
}
if node.Data() == nil {
vcl.ShowMessage("unexpected nil data")
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) OnMnuFileSqliteMemoryClick(sender vcl.IObject) {
f.sqliteAddDatabaseFromFile(`:memory:`)
}
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("https://code.ivysaur.me/yvbolt")
if err != nil {
vcl.ShowMessage("Opening browser: " + err.Error())
}
}
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
}
mnu := vcl.NewPopupMenu(f.Buckets)
mnuRefresh := vcl.NewMenuItem(mnu)
mnuRefresh.SetCaption("Refresh")
mnuRefresh.SetOnClick(f.OnNavContextRefresh)
mnu.Items().Add(mnuRefresh)
// Check what custom actions the ndata->db itself wants to add
// ...
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)
mnu.Items().Add(mnuClose)
}
// Show popup
mnu.Popup2()
}
func (f *TMainForm) OnNavContextRefresh(sender vcl.IObject) {
vcl.ShowMessage("TODO")
// TODO
}
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())
// 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)
ndata.ld.ExecQuery(queryString, f.queryResult)
}
func formatUtf8(in []byte) string {
if !utf8.Valid(in) {
return fmt.Sprintf("<<Invalid UTF-8 %q>>", in)
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
}
return string(in)
}
ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function
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
// 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) {
@@ -259,31 +427,66 @@ func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allo
ndata := (*navData)(node.Data())
if ndata.childrenLoaded {
err := f.NavLoadChildren(node, ndata)
if err != nil {
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 := boltChildBucketNames(ndata.ld.db, ndata.bucketPath)
nextBucketNames, err := ndata.ld.NavChildren(ndata)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to find child buckets under %q: %s", strings.Join(ndata.bucketPath, `/`), err.Error()))
*allowExpansion = false
return
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)
*allowExpansion = 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
@@ -292,36 +495,10 @@ func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allo
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
ndata.ld.Keepalive(navData)
}
*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
return nil
}

5
platform_windows.go Normal file
View File

@@ -0,0 +1,5 @@
package main
import (
_ "github.com/ying32/govcl/pkgs/winappres" // Extra _syso files for Windows
)

130
redisConnectionDialog.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"fmt"
"github.com/redis/go-redis/v9"
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
)
type TRedisConnectionDialogResult = *redis.Options
type TRedisConnectionDialog struct {
*vcl.TForm
Address *vcl.TEdit
Port *vcl.TSpinEdit
Password *vcl.TEdit
IsResp3Protocol *vcl.TCheckBox
ConnectBtn *vcl.TButton
// CustomReturn must be set by the caller to a *redis.Options variable.
CustomReturn *TRedisConnectionDialogResult // nil = no CustomReturn/cancelled dialog
}
func (f *TRedisConnectionDialog) OnFormCreate(sender vcl.IObject) {
f.SetCaption("Connect to Redis...")
f.ScreenCenter()
f.SetWidth(320)
f.SetHeight(160)
// row 1
row1 := vcl_row(f, 1)
lblAddress := vcl.NewLabel(row1)
lblAddress.SetParent(row1)
lblAddress.SetCaption("Address:")
lblAddress.SetAlign(types.AlLeft)
lblAddress.SetLayout(types.TlCenter)
lblAddress.SetLeft(1)
lblAddress.BorderSpacing().SetAround(MY_SPACING)
f.Address = vcl.NewEdit(row1)
f.Address.SetParent(row1)
f.Address.SetAlign(types.AlClient) // grow AlLeft)
f.Address.SetWidth(MY_WIDTH)
f.Address.SetLeft(2)
f.Address.SetText("127.0.0.1")
f.Address.BorderSpacing().SetAround(MY_SPACING)
lblPort := vcl.NewLabel(row1)
lblPort.SetParent(row1)
lblPort.SetCaption(":")
lblPort.SetAlign(types.AlRight)
lblPort.SetLayout(types.TlCenter)
lblPort.SetLeft(0)
lblPort.SetAutoSize(true)
lblPort.BorderSpacing().SetAround(MY_SPACING)
f.Port = vcl.NewSpinEdit(row1)
f.Port.SetParent(row1)
f.Port.SetMinValue(1)
f.Port.SetMaxValue(65535)
f.Port.SetValue(6379) // Redis default port
f.Port.SetAlign(types.AlRight)
f.Port.SetLeft(1000)
f.Port.BorderSpacing().SetAround(MY_SPACING)
// row 2
row2 := vcl_row(f, 2)
lblPassword := vcl.NewLabel(row2)
lblPassword.SetParent(row2)
lblPassword.SetCaption("Password:")
lblPassword.SetAlign(types.AlLeft)
lblPassword.SetLayout(types.TlCenter)
lblPassword.SetLeft(1)
lblPassword.BorderSpacing().SetAround(MY_SPACING)
f.Password = vcl.NewEdit(row2)
f.Password.SetParent(row2)
f.Password.SetAlign(types.AlClient) // grow
f.Password.SetLeft(2)
f.Password.SetWidth(MY_WIDTH)
f.Password.SetPasswordChar(uint16('*'))
f.Password.BorderSpacing().SetAround(MY_SPACING)
// row 3
f.IsResp3Protocol = vcl.NewCheckBox(f)
f.IsResp3Protocol.SetParent(f)
f.IsResp3Protocol.SetCaption("Use RESP v3 protocol")
f.IsResp3Protocol.SetChecked(true)
f.IsResp3Protocol.SetAlign(types.AlTop)
f.IsResp3Protocol.SetTop(3)
f.IsResp3Protocol.BorderSpacing().SetAround(MY_SPACING)
// buttons
row4 := vcl_row(f, 4)
row4.SetAlign(types.AlBottom)
f.ConnectBtn = vcl.NewButton(row4)
f.ConnectBtn.SetParent(row4)
f.ConnectBtn.SetAlign(types.AlRight)
f.ConnectBtn.SetCaption("Connect")
f.ConnectBtn.SetOnClick(f.OnConnectBtnClick)
}
func (f *TRedisConnectionDialog) OnConnectBtnClick(sender vcl.IObject) {
ret := &redis.Options{
Addr: fmt.Sprintf("%s:%d", f.Address.Text(), f.Port.Value()),
Password: f.Password.Text(),
DB: 0, // Start off with default DB, but, we will browse them ourselves
Protocol: 2, // RESP2
}
if f.IsResp3Protocol.Checked() {
ret.Protocol = 3 // RESP3
}
// Success.
// Fill in target pointer and close dialog with "MrOk" sentinel
*f.CustomReturn = ret
f.SetModalResult(types.MrOk)
}

27
util_format.go Normal file
View File

@@ -0,0 +1,27 @@
package main
import (
"fmt"
"unicode/utf8"
)
func formatUtf8(in []byte) string {
if !utf8.Valid(in) {
return fmt.Sprintf("<<Invalid UTF-8 %q>>", in)
}
return string(in)
}
func formatAny(in interface{}) string {
switch in := in.(type) {
case []byte:
return "<<binary>>"
case string:
return in
default:
return fmt.Sprintf("%#v", in)
}
}

11
util_types.go Normal file
View File

@@ -0,0 +1,11 @@
package main
// box_interface creates a slice where all elements of the input slice are boxed
// in an interface{} type.
func box_interface[T any](input []T) []interface{} {
ret := make([]interface{}, 0, len(input))
for _, v := range input {
ret = append(ret, v)
}
return ret
}

27
util_vcl.go Normal file
View File

@@ -0,0 +1,27 @@
package main
import (
"github.com/ying32/govcl/vcl"
"github.com/ying32/govcl/vcl/types"
)
const (
MY_SPACING = 6
MY_HEIGHT = 90
MY_WIDTH = 180
)
// vcl_row makes a TPanel row inside the target component.
func vcl_row(owner vcl.IWinControl, top int32) *vcl.TPanel {
r := vcl.NewPanel(owner)
r.SetParent(owner)
r.SetBorderStyle(types.BsNone)
r.SetBevelInner(types.BvNone)
r.SetBevelOuter(types.BvNone)
r.SetAlign(types.AlTop)
r.SetTop(top)
r.SetAutoSize(true)
return r
}