Compare commits
9 Commits
Author | SHA1 | Date |
---|---|---|
mappu | 18331ae007 | |
mappu | f08242e93c | |
mappu | 23965230e2 | |
mappu | e43b261752 | |
mappu | f9b4cb71a5 | |
mappu | 96c05641bd | |
mappu | 2c5d1946ec | |
mappu | a6cbc5a9ed | |
mappu | 6008ae44a2 |
|
@ -1,4 +1,16 @@
|
|||
build-qbolt-*
|
||||
# development
|
||||
qbolt
|
||||
dummy-data/dummy-data*
|
||||
*.pro.user$
|
||||
build/
|
||||
|
||||
# temporary build files
|
||||
rsrc_windows_amd64.syso
|
||||
windows-manifest.json
|
||||
|
||||
# release build files
|
||||
build/qbolt
|
||||
build/qbolt.exe
|
||||
build/*.xz
|
||||
build/*.zip
|
||||
|
||||
# local makefile definition scripts
|
||||
make-*
|
||||
|
|
108
Makefile
|
@ -1,72 +1,60 @@
|
|||
|
||||
export PATH := /usr/lib/mxe/usr/bin:$(PATH)
|
||||
GOFLAGS := -ldflags='-s -w' -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
|
||||
VERSION := 1.0.2
|
||||
VERSION := 1.0.3
|
||||
GOFLAGS_L := -ldflags='-s -w -X main.Version=v$(VERSION)' -buildvcs=false -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
|
||||
GOFLAGS_W := -ldflags='-s -w -X main.Version=v$(VERSION) -H windowsgui' -buildvcs=false --tags=windowsqtstatic -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
|
||||
SHELL := /bin/bash
|
||||
# Allow overriding DOCKER with e.g. sudo docker
|
||||
DOCKER := docker
|
||||
MIQT_UIC := miqt-uic
|
||||
MIQT_RCC := miqt-rcc
|
||||
GO_WINRES := go-winres
|
||||
SOURCES := $(wildcard *.go *.ui *.qrc) resources.go mainwindow_ui.go itemwindow_ui.go rsrc_windows_amd64.syso
|
||||
|
||||
.PHONY: all libs dist clean
|
||||
|
||||
all: \
|
||||
build/linux/qbolt \
|
||||
build/win32/release/qbolt.exe
|
||||
|
||||
libs: \
|
||||
build/linux/qbolt.a \
|
||||
build/win32/qbolt.a
|
||||
.PHONY: all
|
||||
all: build/qbolt build/qbolt.exe
|
||||
|
||||
dist: \
|
||||
build/dist/qbolt-${VERSION}-win32.zip \
|
||||
build/dist/qbolt-${VERSION}-linux_amd64.tar.xz
|
||||
.PHONY: dist
|
||||
dist: build/qbolt-${VERSION}-windows-x86_64.zip build/qbolt-${VERSION}-debian12-x86_64.tar.xz
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
if [ -f qbolt/qbolt.a ] ; then rm qbolt/qbolt.a ; fi
|
||||
if [ -f qbolt ] ; then rm qbolt ; fi
|
||||
if [ -d build ] ; then rm -r build ; fi
|
||||
rm -f qbolt || true
|
||||
rm -rf build || true
|
||||
rm -f windows-manifest.json || true
|
||||
|
||||
# Build core golang shared library (linux)
|
||||
# Generated files
|
||||
|
||||
build/linux/qbolt.a: *.go
|
||||
mkdir -p build/linux
|
||||
go build ${GOFLAGS} -buildmode=c-archive -o build/linux/qbolt.a
|
||||
resources.rcc resources.go: resources.qrc
|
||||
$(MIQT_RCC) resources.qrc
|
||||
|
||||
# Build core golang shared library (win32)
|
||||
mainwindow_ui.go: mainwindow.ui
|
||||
$(MIQT_UIC) -InFile mainwindow.ui -OutFile mainwindow_ui.go
|
||||
|
||||
itemwindow_ui.go: itemwindow.ui
|
||||
$(MIQT_UIC) -InFile itemwindow.ui -OutFile itemwindow_ui.go
|
||||
|
||||
build/win32/qbolt.a: *.go
|
||||
mkdir -p build/win32
|
||||
CC=/usr/lib/mxe/usr/bin/i686-w64-mingw32.static-gcc CGO_ENABLED=1 GOARCH=386 GOOS=windows \
|
||||
go build ${GOFLAGS} -buildmode=c-archive -o build/win32/qbolt.a
|
||||
|
||||
# Linux binaries
|
||||
windows-manifest.json: windows-manifest.template.json Makefile
|
||||
cat windows-manifest.template.json | sed -re 's_%VERSION%_$(VERSION)_' > windows-manifest.json
|
||||
|
||||
build/linux/qbolt: build/linux/qbolt.a qbolt/*
|
||||
cd build/linux && qmake ../../qbolt/qbolt.pro && make
|
||||
|
||||
# Linux distribution
|
||||
|
||||
build/dist/qbolt-${VERSION}-linux_amd64.tar.xz: build/linux/qbolt
|
||||
XZ_OPTS=-9 tar caf build/dist/qbolt-${VERSION}-linux_amd64.tar.xz -C build/linux qbolt --owner=0 --group=0
|
||||
rsrc_windows_amd64.syso: windows-manifest.json
|
||||
$(GO_WINRES) make --in windows-manifest.json
|
||||
rm rsrc_windows_386.syso || true # we do not build x86_32
|
||||
|
||||
# Windows binaries
|
||||
|
||||
build/win32/release/qbolt.exe: build/win32/qbolt.a qbolt/*
|
||||
cd build/win32 && i686-w64-mingw32.static-qmake-qt5 ../../qbolt/qbolt.pro && make
|
||||
|
||||
# Dockerized Windows build
|
||||
|
||||
.PHONY: build-docker-build-environment
|
||||
build-docker-build-environment:
|
||||
cd docker && docker build -t win32-cross-qt-mxe:latest -f win32-cross-qt-mxe.Dockerfile
|
||||
|
||||
.PHONY: build-windows-in-docker
|
||||
build-windows-in-docker:
|
||||
docker run --rm -v $(CURDIR):/qbolt win32-cross-qt-mxe:latest /bin/sh -c 'cd /qbolt && make build/win32/release/qbolt.exe'
|
||||
|
||||
# Windows distribution
|
||||
|
||||
build/win32/dist/qbolt.exe: build/win32/release/qbolt.exe
|
||||
mkdir -p build/win32/dist
|
||||
cp build/win32/release/qbolt.exe build/win32/dist/qbolt.exe
|
||||
# upx --lzma build/win32/dist/qbolt.exe
|
||||
# Linux release
|
||||
|
||||
build/dist/qbolt-${VERSION}-win32.zip: build/win32/dist/qbolt.exe
|
||||
mkdir -p build/dist
|
||||
zip -0 -j build/dist/qbolt-${VERSION}-win32.zip build/win32/dist/qbolt.exe
|
||||
build/qbolt: $(SOURCES)
|
||||
go build $(GOFLAGS_L) -o build/qbolt
|
||||
upx build/qbolt
|
||||
|
||||
build/qbolt-${VERSION}-debian12-x86_64.tar.xz: build/qbolt
|
||||
XZ_OPTS=-9e tar caf build/qbolt-${VERSION}-debian12-x86_64.tar.xz -C build qbolt --owner=0 --group=0
|
||||
|
||||
# Windows release (docker)
|
||||
|
||||
build/qbolt.exe: $(SOURCES)
|
||||
( $(DOCKER) image ls | fgrep qbolt-win64-cross ) || ( cd docker && $(DOCKER) build -t qbolt-win64-cross:latest -f win64-cross.Dockerfile . )
|
||||
$(DOCKER) run --rm -v $(CURDIR):/qbolt -w /qbolt qbolt-win64-cross:latest /bin/sh -c "go build $(GOFLAGS_W) -o build/qbolt.exe"
|
||||
upx --force build/qbolt.exe
|
||||
|
||||
build/qbolt-${VERSION}-windows-x86_64.zip: build/qbolt.exe
|
||||
zip -9 -j build/qbolt-${VERSION}-windows-x86_64.zip build/qbolt.exe
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ObjectReference int64
|
||||
|
||||
var NullObjectReference error = errors.New("Null object reference")
|
||||
|
||||
// GoMemoryStore is a int->interface storage structure so that Go pointers are
|
||||
// never exposed to C code.
|
||||
type GoMemoryStore struct {
|
||||
mtx sync.RWMutex
|
||||
items map[int64]interface{}
|
||||
next int64
|
||||
}
|
||||
|
||||
func NewGoMemoryStore() *GoMemoryStore {
|
||||
ret := GoMemoryStore{}
|
||||
ret.items = make(map[int64]interface{})
|
||||
return &ret
|
||||
}
|
||||
|
||||
func (this *GoMemoryStore) Put(itm interface{}) ObjectReference {
|
||||
this.mtx.Lock()
|
||||
defer this.mtx.Unlock()
|
||||
|
||||
key := this.next
|
||||
this.items[key] = itm
|
||||
this.next++
|
||||
return ObjectReference(key)
|
||||
}
|
||||
|
||||
func (this *GoMemoryStore) Get(i ObjectReference) (interface{}, bool) {
|
||||
this.mtx.RLock()
|
||||
defer this.mtx.RUnlock()
|
||||
|
||||
ret, ok := this.items[int64(i)]
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
func (this *GoMemoryStore) Delete(i ObjectReference) {
|
||||
this.mtx.Lock()
|
||||
defer this.mtx.Unlock()
|
||||
|
||||
delete(this.items, int64(i))
|
||||
}
|
||||
|
||||
var gms *GoMemoryStore = nil
|
||||
|
||||
func init() {
|
||||
gms = NewGoMemoryStore()
|
||||
}
|
11
README.md
|
@ -4,9 +4,7 @@ A graphical database manager for BoltDB.
|
|||
|
||||
QBolt allows you to graphically view and edit the content of Bolt databases.
|
||||
|
||||
The project consists of two parts; a C binding (CGo) for the embeddable Bolt database engine, and a graphical interface built in C++/Qt that links to it.
|
||||
|
||||
Written in C++ (Qt), Golang (CGo)
|
||||
Written in Golang (Qt)
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -30,6 +28,12 @@ The Windows binary is released under LGPL-3+ owing to the static copy of Qt.
|
|||
|
||||
## Changelog
|
||||
|
||||
2024-10-05 1.0.3
|
||||
- Port from hybrid Go/C++ to now using [MIQT](https://github.com/mappu/miqt)
|
||||
- Switch Windows build to win64
|
||||
- Rebuild artefacts with miqt v0.5.0, etcd-io/bbolt v1.3.11, go 1.19 (deb12), go 1.23 (win64)
|
||||
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.3)
|
||||
|
||||
2020-04-12 1.0.2
|
||||
- Rebuild artefacts with etcd-io/bbolt v1.3.5, go 1.15, Qt 5.15, and new GCC versions
|
||||
- Switch from hg to Git
|
||||
|
@ -46,4 +50,5 @@ The Windows binary is released under LGPL-3+ owing to the static copy of Qt.
|
|||
|
||||
2017-05-21 1.0.0
|
||||
- Initial public release
|
||||
- The project consists of two parts; a C binding (CGo) for the embeddable Bolt database engine, and a graphical interface built in C++/Qt that links to it.
|
||||
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.0)
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func Bolt_Open(readOnly bool, path string) (*bolt.DB, error) {
|
||||
opts := *bolt.DefaultOptions
|
||||
opts.Timeout = 10 * time.Second
|
||||
opts.ReadOnly = readOnly
|
||||
|
||||
return bolt.Open(path, os.FileMode(0644), &opts)
|
||||
}
|
||||
|
||||
func walkBuckets(tx *bolt.Tx, browse []string) (*bolt.Bucket, error) {
|
||||
bucket := tx.Bucket([]byte(browse[0]))
|
||||
if bucket == nil {
|
||||
return nil, errors.New("Unknown bucket")
|
||||
}
|
||||
|
||||
for i := 1; i < len(browse); i += 1 {
|
||||
bucket = bucket.Bucket([]byte(browse[i]))
|
||||
if bucket == nil {
|
||||
return nil, errors.New("Unknown bucket")
|
||||
}
|
||||
}
|
||||
|
||||
return bucket, nil
|
||||
}
|
||||
|
||||
func withBrowse_ReadOnly(db *bolt.DB, browse []string, fn func(tx *bolt.Tx, bucket *bolt.Bucket) error) error {
|
||||
if len(browse) == 0 {
|
||||
// not a bucket
|
||||
return errors.New("No bucket selected")
|
||||
}
|
||||
|
||||
return db.View(func(tx *bolt.Tx) error {
|
||||
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walked the bucket chain, now run the user callback
|
||||
return fn(tx, bucket)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func Bolt_CreateBucket(db *bolt.DB, browse []string, newBucket string) error {
|
||||
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
if len(browse) == 0 {
|
||||
// Top-level bucket
|
||||
_, err := tx.CreateBucket([]byte(newBucket))
|
||||
return err
|
||||
|
||||
} else {
|
||||
// Deeper bucket
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walked the bucket chain, now create the new bucket
|
||||
_, err = bucket.CreateBucket([]byte(newBucket))
|
||||
return err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Bolt_DeleteBucket(db *bolt.DB, browse []string, delBucket string) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
if len(browse) == 0 {
|
||||
// Top-level bucket
|
||||
return tx.DeleteBucket([]byte(delBucket))
|
||||
|
||||
} else {
|
||||
// Deeper bucket
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walked the bucket chain, now delete the selected bucket
|
||||
return bucket.DeleteBucket([]byte(delBucket))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Bolt_SetItem(db *bolt.DB, browse []string, key, val string) error {
|
||||
if len(browse) == 0 {
|
||||
return errors.New("Can't create top-level items")
|
||||
}
|
||||
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put([]byte(key), []byte(val))
|
||||
})
|
||||
}
|
||||
|
||||
func Bolt_DeleteItem(db *bolt.DB, browse []string, key string) error {
|
||||
if len(browse) == 0 {
|
||||
return errors.New("Can't create top-level items")
|
||||
}
|
||||
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Delete([]byte(key))
|
||||
})
|
||||
}
|
||||
|
||||
func Bolt_DBStats(db *bolt.DB) (string, error) {
|
||||
jBytes, err := json.MarshalIndent(db.Stats(), "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jBytes), nil
|
||||
}
|
||||
|
||||
func Bolt_BucketStats(db *bolt.DB, browse []string) (string, error) {
|
||||
var stats bolt.BucketStats
|
||||
|
||||
err := withBrowse_ReadOnly(db, browse, func(tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
stats = bucket.Stats()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jBytes, err := json.MarshalIndent(stats, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jBytes), err
|
||||
}
|
||||
|
||||
func Bolt_ListBuckets(db *bolt.DB, browse []string, cb func(b string)) error {
|
||||
|
||||
if len(browse) == 0 {
|
||||
// root mode
|
||||
return db.View(func(tx *bolt.Tx) error {
|
||||
return tx.ForEach(func(k []byte, _ *bolt.Bucket) error {
|
||||
cb(string(k))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// Nested-mode
|
||||
return withBrowse_ReadOnly(db, browse, func(tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
return bucket.ForEach(func(k, v []byte) error {
|
||||
// non-nil v means it's a data item
|
||||
if v == nil {
|
||||
cb(string(k))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type ListItemInfo struct {
|
||||
Name string
|
||||
DataLen int64
|
||||
}
|
||||
|
||||
func Bolt_ListItems(db *bolt.DB, browse []string, cb func(ListItemInfo) error) error {
|
||||
|
||||
if len(browse) == 0 {
|
||||
return errors.New("No bucket specified")
|
||||
}
|
||||
|
||||
// Nested-mode
|
||||
return withBrowse_ReadOnly(db, browse, func(tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
return bucket.ForEach(func(k, v []byte) error {
|
||||
if v == nil {
|
||||
return nil // nil v means it's a bucket, skip
|
||||
}
|
||||
|
||||
return cb(ListItemInfo{string(k), int64(len(v))})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Bolt_GetItem(db *bolt.DB, browse []string, key string) (string, error) {
|
||||
var ret string
|
||||
|
||||
err := withBrowse_ReadOnly(db, browse, func(tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
d := bucket.Get([]byte(key))
|
||||
ret = string(d)
|
||||
return nil
|
||||
})
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func Bolt_Close(db *bolt.DB) error {
|
||||
return db.Close()
|
||||
}
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
@ -1,13 +1,20 @@
|
|||
FROM debian:bullseye
|
||||
FROM golang:1.23-bookworm
|
||||
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
|
||||
apt-get install -qyy gnupg2 golang-go ca-certificates
|
||||
|
||||
apt-get install -qyy gnupg2 ca-certificates
|
||||
|
||||
RUN DEBIAN_FRONTEND=noninteractive \
|
||||
echo "deb https://pkg.mxe.cc/repos/apt buster main" >/etc/apt/sources.list.d/mxeapt.list && \
|
||||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 86B72ED9 && \
|
||||
apt-get update && \
|
||||
apt-get install -qyy mxe-i686-w64-mingw32.static-qt5 && \
|
||||
apt-get install -qyy mxe-x86-64-w64-mingw32.static-qt5 && \
|
||||
apt-get clean
|
||||
|
||||
ENV PATH=/usr/lib/mxe/usr/bin:$PATH
|
||||
|
||||
ENV CXX=x86_64-w64-mingw32.static-g++
|
||||
ENV CC=x86_64-w64-mingw32.static-gcc
|
||||
ENV PKG_CONFIG=x86_64-w64-mingw32.static-pkg-config
|
||||
ENV GOOS=windows
|
||||
ENV CGO_ENABLED=1
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
//"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
|
||||
bolt "github.com/boltdb/bolt"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func random_name() string {
|
||||
|
|
9
go.mod
|
@ -1,7 +1,10 @@
|
|||
module code.ivysaur.me/qbolt
|
||||
|
||||
go 1.15
|
||||
go 1.23
|
||||
|
||||
require github.com/boltdb/bolt v1.3.1
|
||||
require (
|
||||
github.com/mappu/miqt v0.5.0
|
||||
go.etcd.io/bbolt v1.3.11
|
||||
)
|
||||
|
||||
replace github.com/boltdb/bolt => go.etcd.io/bbolt v1.3.5
|
||||
require golang.org/x/sys v0.4.0 // indirect
|
||||
|
|
16
go.sum
|
@ -1,5 +1,11 @@
|
|||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/mappu/miqt v0.5.0 h1:BWajkNI9PWlWN6ZDgWKwv1gieBGEImRqlWS8ZqDmDfA=
|
||||
github.com/mappu/miqt v0.5.0/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
||||
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
|
|
@ -42,12 +42,6 @@
|
|||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
|
@ -0,0 +1,71 @@
|
|||
// Generated by miqt-uic. To update this file, edit the .ui file in
|
||||
// Qt Designer, and then run 'go generate'.
|
||||
//
|
||||
//go:generate miqt-uic -InFile itemwindow.ui -OutFile itemwindow_ui.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mappu/miqt/qt"
|
||||
)
|
||||
|
||||
type ItemWindowUi struct {
|
||||
ItemWindow *qt.QDialog
|
||||
gridLayout_2 *qt.QGridLayout
|
||||
contentArea *qt.QPlainTextEdit
|
||||
frame *qt.QFrame
|
||||
gridLayout *qt.QGridLayout
|
||||
buttonBox *qt.QDialogButtonBox
|
||||
}
|
||||
|
||||
// NewItemWindowUi creates all Qt widget classes for ItemWindow.
|
||||
func NewItemWindowUi() *ItemWindowUi {
|
||||
ui := &ItemWindowUi{}
|
||||
|
||||
ui.ItemWindow = qt.NewQDialog2(nil)
|
||||
ui.ItemWindow.SetObjectName("ItemWindow")
|
||||
ui.ItemWindow.Resize(370, 353)
|
||||
ui.ItemWindow.SetWindowTitle("")
|
||||
icon0 := qt.NewQIcon()
|
||||
icon0.AddFile4(":/rsrc/database_lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.ItemWindow.SetWindowIcon(icon0)
|
||||
|
||||
ui.gridLayout_2 = qt.NewQGridLayout(ui.ItemWindow.QWidget)
|
||||
ui.gridLayout_2.SetObjectName("gridLayout_2")
|
||||
ui.gridLayout_2.SetVerticalSpacing(0)
|
||||
ui.gridLayout_2.SetContentsMargins(0, 0, 0, 0)
|
||||
ui.gridLayout_2.SetSpacing(6)
|
||||
|
||||
ui.contentArea = qt.NewQPlainTextEdit3(ui.ItemWindow.QWidget)
|
||||
ui.contentArea.SetObjectName("contentArea")
|
||||
ui.contentArea.SetFrameShape(qt.QFrame__NoFrame)
|
||||
|
||||
ui.gridLayout_2.AddWidget2(ui.contentArea.QWidget, 0, 0)
|
||||
|
||||
ui.frame = qt.NewQFrame2(ui.ItemWindow.QWidget)
|
||||
ui.frame.SetObjectName("frame")
|
||||
|
||||
ui.gridLayout = qt.NewQGridLayout(ui.frame.QWidget)
|
||||
ui.gridLayout.SetObjectName("gridLayout")
|
||||
ui.gridLayout.SetContentsMargins(11, 11, 11, 11)
|
||||
ui.gridLayout.SetSpacing(6)
|
||||
|
||||
ui.buttonBox = qt.NewQDialogButtonBox5(ui.frame.QWidget)
|
||||
ui.buttonBox.SetObjectName("buttonBox")
|
||||
ui.buttonBox.SetStandardButtons(qt.QDialogButtonBox__Cancel | qt.QDialogButtonBox__Save)
|
||||
ui.buttonBox.OnAccepted(ui.ItemWindow.Accept)
|
||||
ui.buttonBox.OnRejected(ui.ItemWindow.Reject)
|
||||
|
||||
ui.gridLayout.AddWidget2(ui.buttonBox.QWidget, 0, 0)
|
||||
|
||||
ui.gridLayout_2.AddWidget2(ui.frame.QWidget, 1, 0)
|
||||
|
||||
ui.Retranslate()
|
||||
|
||||
return ui
|
||||
}
|
||||
|
||||
// Retranslate reapplies all text translations.
|
||||
func (ui *ItemWindowUi) Retranslate() {
|
||||
|
||||
}
|
400
main.go
|
@ -1,398 +1,22 @@
|
|||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
bolt "github.com/boltdb/bolt"
|
||||
"github.com/mappu/miqt/qt"
|
||||
)
|
||||
|
||||
const (
|
||||
ERROR_AND_STOP_CALLING int64 = 100
|
||||
ERROR_AND_KEEP_CALLING = 101
|
||||
FINISHED_OK = 102
|
||||
REAL_MESSAGE = 103
|
||||
)
|
||||
|
||||
const Magic int64 = 0x10203040
|
||||
|
||||
//export GetMagic
|
||||
func GetMagic() int64 {
|
||||
return Magic
|
||||
}
|
||||
|
||||
//export Bolt_Open
|
||||
func Bolt_Open(readOnly bool, path string) (ObjectReference, *C.char, int) {
|
||||
opts := *bolt.DefaultOptions
|
||||
opts.Timeout = 10 * time.Second
|
||||
opts.ReadOnly = readOnly
|
||||
|
||||
ptrDB, err := bolt.Open(path, os.FileMode(0644), &opts)
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
return 0, C.CString(errMsg), len(errMsg)
|
||||
}
|
||||
|
||||
dbRef := gms.Put(ptrDB)
|
||||
return dbRef, nil, 0
|
||||
}
|
||||
|
||||
func withBoltDBReference(b ObjectReference, fn func(db *bolt.DB) error) error {
|
||||
dbIFC, ok := gms.Get(b)
|
||||
if !ok {
|
||||
return NullObjectReference
|
||||
}
|
||||
|
||||
ptrDB, ok := dbIFC.(*bolt.DB)
|
||||
if !ok {
|
||||
return NullObjectReference
|
||||
}
|
||||
|
||||
return fn(ptrDB)
|
||||
}
|
||||
|
||||
func walkBuckets(tx *bolt.Tx, browse []string) (*bolt.Bucket, error) {
|
||||
bucket := tx.Bucket([]byte(browse[0]))
|
||||
if bucket == nil {
|
||||
return nil, errors.New("Unknown bucket")
|
||||
}
|
||||
|
||||
for i := 1; i < len(browse); i += 1 {
|
||||
bucket = bucket.Bucket([]byte(browse[i]))
|
||||
if bucket == nil {
|
||||
return nil, errors.New("Unknown bucket")
|
||||
}
|
||||
}
|
||||
|
||||
return bucket, nil
|
||||
}
|
||||
|
||||
func withBrowse_ReadOnly(b_ref ObjectReference, browse []string, fn func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error) error {
|
||||
if len(browse) == 0 {
|
||||
// not a bucket
|
||||
return errors.New("No bucket selected")
|
||||
}
|
||||
|
||||
return withBoltDBReference(b_ref, func(db *bolt.DB) error {
|
||||
return db.View(func(tx *bolt.Tx) error {
|
||||
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walked the bucket chain, now run the user callback
|
||||
return fn(db, tx, bucket)
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func err2triple(err error) (int64, *C.char, int) {
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
return ERROR_AND_STOP_CALLING, C.CString(msg), len(msg)
|
||||
}
|
||||
|
||||
return FINISHED_OK, nil, 0
|
||||
}
|
||||
|
||||
//export Bolt_CreateBucket
|
||||
func Bolt_CreateBucket(b_ref ObjectReference, browse []string, newBucket string) (int64, *C.char, int) {
|
||||
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
if len(browse) == 0 {
|
||||
// Top-level bucket
|
||||
_, err := tx.CreateBucket([]byte(newBucket))
|
||||
return err
|
||||
|
||||
} else {
|
||||
// Deeper bucket
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walked the bucket chain, now create the new bucket
|
||||
_, err = bucket.CreateBucket([]byte(newBucket))
|
||||
return err
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
//export Bolt_DeleteBucket
|
||||
func Bolt_DeleteBucket(b_ref ObjectReference, browse []string, delBucket string) (int64, *C.char, int) {
|
||||
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
if len(browse) == 0 {
|
||||
// Top-level bucket
|
||||
return tx.DeleteBucket([]byte(delBucket))
|
||||
|
||||
} else {
|
||||
// Deeper bucket
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walked the bucket chain, now delete the selected bucket
|
||||
return bucket.DeleteBucket([]byte(delBucket))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
//export Bolt_SetItem
|
||||
func Bolt_SetItem(b_ref ObjectReference, browse []string, key, val string) (int64, *C.char, int) {
|
||||
if len(browse) == 0 {
|
||||
return err2triple(errors.New("Can't create top-level items"))
|
||||
}
|
||||
|
||||
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put([]byte(key), []byte(val))
|
||||
})
|
||||
})
|
||||
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
//export Bolt_DeleteItem
|
||||
func Bolt_DeleteItem(b_ref ObjectReference, browse []string, key string) (int64, *C.char, int) {
|
||||
if len(browse) == 0 {
|
||||
return err2triple(errors.New("Can't create top-level items"))
|
||||
}
|
||||
|
||||
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Delete([]byte(key))
|
||||
})
|
||||
})
|
||||
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
type CallResponse struct {
|
||||
s string
|
||||
e error
|
||||
}
|
||||
|
||||
//export Bolt_DBStats
|
||||
func Bolt_DBStats(b ObjectReference) (int64, *C.char, int) {
|
||||
var stats bolt.Stats
|
||||
|
||||
err := withBoltDBReference(b, func(db *bolt.DB) error {
|
||||
stats = db.Stats()
|
||||
return nil
|
||||
})
|
||||
|
||||
jBytes, err := json.Marshal(stats)
|
||||
if err != nil {
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
return REAL_MESSAGE, C.CString(string(jBytes)), len(jBytes)
|
||||
}
|
||||
|
||||
//export Bolt_BucketStats
|
||||
func Bolt_BucketStats(b ObjectReference, browse []string) (int64, *C.char, int) {
|
||||
var stats bolt.BucketStats
|
||||
|
||||
err := withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
stats = bucket.Stats()
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
jBytes, err := json.Marshal(stats)
|
||||
if err != nil {
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
return REAL_MESSAGE, C.CString(string(jBytes)), len(jBytes)
|
||||
}
|
||||
|
||||
type NextCall struct {
|
||||
content chan CallResponse
|
||||
}
|
||||
|
||||
//export Bolt_ListBuckets
|
||||
func Bolt_ListBuckets(b ObjectReference, browse []string) ObjectReference {
|
||||
pNC := &NextCall{
|
||||
content: make(chan CallResponse, 0),
|
||||
}
|
||||
|
||||
pNC_Ref := gms.Put(pNC)
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
|
||||
if len(browse) == 0 {
|
||||
// root mode
|
||||
err = withBoltDBReference(b, func(db *bolt.DB) error {
|
||||
return db.View(func(tx *bolt.Tx) error {
|
||||
return tx.ForEach(func(k []byte, _ *bolt.Bucket) error {
|
||||
pNC.content <- CallResponse{s: string(k)}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
} else {
|
||||
// Nested-mode
|
||||
err = withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
return bucket.ForEach(func(k, v []byte) error {
|
||||
// non-nil v means it's a data item
|
||||
if v == nil {
|
||||
pNC.content <- CallResponse{s: string(k)}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
pNC.content <- CallResponse{e: err}
|
||||
}
|
||||
|
||||
close(pNC.content)
|
||||
}()
|
||||
|
||||
return pNC_Ref
|
||||
}
|
||||
|
||||
//export Bolt_ListItems
|
||||
func Bolt_ListItems(b ObjectReference, browse []string) ObjectReference {
|
||||
|
||||
pNC := &NextCall{
|
||||
content: make(chan CallResponse, 0),
|
||||
}
|
||||
|
||||
pNC_Ref := gms.Put(pNC)
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
|
||||
if len(browse) == 0 {
|
||||
err = errors.New("No bucket specified")
|
||||
} else {
|
||||
// Nested-mode
|
||||
err = withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
return bucket.ForEach(func(k, v []byte) error {
|
||||
if v == nil {
|
||||
return nil // nil v means it's a bucket, skip
|
||||
}
|
||||
|
||||
itemLength := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(itemLength, uint64(len(v)))
|
||||
pNC.content <- CallResponse{s: string(itemLength) + string(k)}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
pNC.content <- CallResponse{e: err}
|
||||
}
|
||||
|
||||
close(pNC.content)
|
||||
}()
|
||||
|
||||
return pNC_Ref
|
||||
}
|
||||
|
||||
//export Bolt_GetItem
|
||||
func Bolt_GetItem(b ObjectReference, browse []string, key string) (int64, *C.char, int) {
|
||||
var ret *C.char = nil
|
||||
var ret_len = 0
|
||||
|
||||
err := withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
d := bucket.Get([]byte(key))
|
||||
ret = C.CString(string(d))
|
||||
ret_len = len(d)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
return REAL_MESSAGE, ret, ret_len
|
||||
}
|
||||
|
||||
//export GetNext
|
||||
func GetNext(oRef ObjectReference) (int64, *C.char, int) {
|
||||
pNC_Iface, ok := gms.Get(oRef)
|
||||
if !ok {
|
||||
return err2triple(NullObjectReference)
|
||||
}
|
||||
|
||||
pNC, ok := pNC_Iface.(*NextCall)
|
||||
if !ok {
|
||||
return err2triple(NullObjectReference)
|
||||
}
|
||||
|
||||
cr, ok := <-pNC.content
|
||||
if !ok {
|
||||
gms.Delete(oRef)
|
||||
return err2triple(nil)
|
||||
}
|
||||
|
||||
if cr.e != nil {
|
||||
msg := cr.e.Error()
|
||||
return ERROR_AND_KEEP_CALLING, C.CString(msg), len(msg)
|
||||
}
|
||||
|
||||
return REAL_MESSAGE, C.CString(cr.s), len(cr.s)
|
||||
}
|
||||
|
||||
//export Bolt_ListBucketsAtRoot
|
||||
func Bolt_ListBucketsAtRoot(b ObjectReference) ObjectReference {
|
||||
return Bolt_ListBuckets(b, nil)
|
||||
}
|
||||
|
||||
//export Bolt_Close
|
||||
func Bolt_Close(b ObjectReference) (*C.char, int) {
|
||||
err := withBoltDBReference(b, func(db *bolt.DB) error {
|
||||
return db.Close()
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
return C.CString(msg), len(msg)
|
||||
}
|
||||
|
||||
gms.Delete(b)
|
||||
return nil, 0
|
||||
}
|
||||
var Version string = "v0.0.0-devel"
|
||||
|
||||
func main() {
|
||||
// virtual
|
||||
|
||||
_ = qt.NewQApplication(os.Args)
|
||||
|
||||
qt.QGuiApplication_SetApplicationDisplayName("QBolt")
|
||||
qt.QGuiApplication_SetWindowIcon(qt.NewQIcon4(":/rsrc/database_lightning.png"))
|
||||
|
||||
w := NewMainWindow()
|
||||
w.ui.MainWindow.Show()
|
||||
|
||||
qt.QApplication_Exec()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,496 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mappu/miqt/qt"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type MainWindow struct {
|
||||
ui *MainWindowUi
|
||||
|
||||
databaseContext *qt.QMenu
|
||||
bucketContext *qt.QMenu
|
||||
lastContextSelection *qt.QTreeWidgetItem
|
||||
}
|
||||
|
||||
func NewMainWindow() *MainWindow {
|
||||
this := &MainWindow{}
|
||||
this.ui = NewMainWindowUi()
|
||||
|
||||
this.on_bucketTree_currentItemChanged(nil, nil)
|
||||
|
||||
this.databaseContext = qt.NewQMenu()
|
||||
this.databaseContext.QWidget.AddAction(this.ui.actionRefresh_buckets)
|
||||
this.databaseContext.QWidget.AddAction(this.ui.actionAdd_bucket)
|
||||
this.databaseContext.AddSeparator()
|
||||
this.databaseContext.QWidget.AddAction(this.ui.actionDisconnect)
|
||||
|
||||
this.bucketContext = qt.NewQMenu()
|
||||
this.bucketContext.QWidget.AddAction(this.ui.actionRefresh_buckets)
|
||||
this.bucketContext.QWidget.AddAction(this.ui.actionAdd_bucket)
|
||||
this.bucketContext.AddSeparator()
|
||||
this.bucketContext.QWidget.AddAction(this.ui.actionDelete_bucket)
|
||||
|
||||
// Connections
|
||||
this.ui.actionNew_database.OnTriggered(this.on_actionNew_database_triggered)
|
||||
this.ui.actionOpen_database.OnTriggered(this.on_actionOpen_database_triggered)
|
||||
this.ui.actionOpen_database_as_read_only.OnTriggered(this.on_actionOpen_database_as_read_only_triggered)
|
||||
this.ui.actionExit.OnTriggered(this.on_actionExit_triggered)
|
||||
this.ui.actionAbout_Qt.OnTriggered(this.on_actionAbout_Qt_triggered)
|
||||
this.ui.actionAbout_qbolt.OnTriggered(this.on_actionAbout_qbolt_triggered)
|
||||
this.ui.actionDisconnect.OnTriggered(this.on_actionDisconnect_triggered)
|
||||
this.ui.bucketTree.OnCustomContextMenuRequested(this.on_bucketTree_customContextMenuRequested)
|
||||
this.ui.actionRefresh_buckets.OnTriggered(this.on_actionRefresh_buckets_triggered)
|
||||
this.ui.bucketTree.OnCurrentItemChanged(this.on_bucketTree_currentItemChanged)
|
||||
this.ui.actionClear_selection.OnTriggered(this.on_actionClear_selection_triggered)
|
||||
this.ui.bucketData.OnDoubleClicked(this.on_bucketData_doubleClicked)
|
||||
this.ui.actionAdd_bucket.OnTriggered(this.on_actionAdd_bucket_triggered)
|
||||
this.ui.actionDelete_bucket.OnTriggered(this.on_actionDelete_bucket_triggered)
|
||||
this.ui.AddDataButton.OnClicked(this.on_AddDataButton_clicked)
|
||||
this.ui.DeleteDataButton.OnClicked(this.on_DeleteDataButton_clicked)
|
||||
this.ui.bucketData.OnItemSelectionChanged(this.on_bucketData_itemSelectionChanged)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
const (
|
||||
BdbPointerRole = int(qt.UserRole + 1)
|
||||
BinaryDataRole = int(qt.UserRole + 2)
|
||||
)
|
||||
|
||||
var bdbs []*bolt.DB = nil
|
||||
|
||||
func SET_BDB(top *qt.QTreeWidgetItem, bdb *bolt.DB) {
|
||||
idx := len(bdbs)
|
||||
bdbs = append(bdbs, bdb)
|
||||
top.SetData(0, BdbPointerRole, qt.NewQVariant7(idx)) // Don't store a Go pointer in Qt memory
|
||||
}
|
||||
|
||||
func GET_BDB(top *qt.QTreeWidgetItem) *bolt.DB {
|
||||
if top == nil {
|
||||
panic("Passed a nil QTreeWidgetItem")
|
||||
}
|
||||
|
||||
dataVariant := top.Data(0, BdbPointerRole)
|
||||
if dataVariant == nil {
|
||||
panic("Selected item has no bdb")
|
||||
}
|
||||
|
||||
return bdbs[dataVariant.ToInt()]
|
||||
}
|
||||
|
||||
func (this *MainWindow) Widget() *qt.QWidget {
|
||||
return this.ui.centralWidget
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_actionNew_database_triggered() {
|
||||
file := qt.QFileDialog_GetSaveFileName2(this.Widget(), "Save new bolt database as...")
|
||||
if len(file) > 0 {
|
||||
this.openDatabase(file, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_actionOpen_database_triggered() {
|
||||
file := qt.QFileDialog_GetOpenFileName2(this.Widget(), "Select bolt database...")
|
||||
if len(file) > 0 {
|
||||
this.openDatabase(file, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_actionOpen_database_as_read_only_triggered() {
|
||||
file := qt.QFileDialog_GetOpenFileName2(this.Widget(), "Select bolt database...")
|
||||
if len(file) > 0 {
|
||||
this.openDatabase(file, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (this *MainWindow) alert(message string) {
|
||||
qt.QMessageBox_Critical(this.Widget(), "qbolt", message)
|
||||
}
|
||||
|
||||
func (this *MainWindow) openDatabase(file string, readOnly bool) {
|
||||
|
||||
// Open
|
||||
bdb, err := Bolt_Open(readOnly, file)
|
||||
if err != nil {
|
||||
this.alert(fmt.Sprintf("Error opening database: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
top := qt.NewQTreeWidgetItem()
|
||||
top.SetText(0, filepath.Base(file))
|
||||
top.SetIcon(0, qt.NewQIcon4(":/rsrc/database.png"))
|
||||
SET_BDB(top, bdb)
|
||||
this.ui.bucketTree.AddTopLevelItem(top)
|
||||
|
||||
this.refreshBucketTree(top)
|
||||
this.ui.bucketTree.SetCurrentItem(top)
|
||||
|
||||
this.ui.bucketTree.ExpandItem(top)
|
||||
}
|
||||
|
||||
func getDisplayName(qba string) string {
|
||||
if qba == "" {
|
||||
return "<empty>"
|
||||
}
|
||||
|
||||
ret := strconv.Quote(qba)
|
||||
return ret[1 : len(ret)-1]
|
||||
}
|
||||
|
||||
func (this *MainWindow) refreshBucketTree(itm *qt.QTreeWidgetItem) {
|
||||
ws := this.getSelection(itm)
|
||||
|
||||
// Remove existing children
|
||||
i := itm.ChildCount()
|
||||
for i > 0 {
|
||||
itm.TakeChild(i - 1).Delete()
|
||||
i -= 1
|
||||
}
|
||||
|
||||
err := Bolt_ListBuckets(ws.bdb, ws.browse, func(qba string) {
|
||||
|
||||
child := qt.NewQTreeWidgetItem6(itm) // NewQTreeWidgetItem()
|
||||
child.SetText(0, getDisplayName(qba))
|
||||
child.SetData(0, BinaryDataRole, qt.NewQVariant15(MakeQByteArray(qba)))
|
||||
child.SetIcon(0, qt.NewQIcon4(":/rsrc/table.png"))
|
||||
|
||||
itm.AddChild(child)
|
||||
this.refreshBucketTree(child)
|
||||
})
|
||||
if err != nil {
|
||||
this.alert(fmt.Sprintf("Error listing buckets under %s: %s", strings.Join(ws.browse, `/`), err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_actionExit_triggered() {
|
||||
this.ui.MainWindow.Close()
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_actionAbout_Qt_triggered() {
|
||||
qt.QApplication_AboutQt()
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_actionAbout_qbolt_triggered() {
|
||||
qt.QMessageBox_About(
|
||||
this.Widget(),
|
||||
qt.QGuiApplication_ApplicationDisplayName(),
|
||||
"<b>QBolt "+Version+"</b><br>Graphical interface for managing Bolt databases<br><br>"+
|
||||
"- <a href='https://github.com/boltdb/bolt'>About BoltDB</a><br>"+
|
||||
"- <a href='http://www.famfamfam.com/lab/icons/silk/'>FamFamFam "Silk" icon set</a><br>"+
|
||||
"- <a href='https://code.ivysaur.me/qbolt'>QBolt homepage</a><br>",
|
||||
)
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_actionDisconnect_triggered() {
|
||||
top := this.lastContextSelection
|
||||
if top.Parent() != nil {
|
||||
return // somehow we didn't select a top-level item
|
||||
}
|
||||
|
||||
bdb := GET_BDB(top)
|
||||
|
||||
// Remove UI
|
||||
this.ui.bucketTree.ClearSelection()
|
||||
top.Delete()
|
||||
|
||||
// Disconnect from DB
|
||||
bdb.Close()
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_bucketTree_customContextMenuRequested(pos *qt.QPoint) {
|
||||
|
||||
itm := this.ui.bucketTree.ItemAt(pos)
|
||||
if itm == nil {
|
||||
return
|
||||
}
|
||||
|
||||
this.lastContextSelection = itm
|
||||
|
||||
if itm.Parent() != nil {
|
||||
// Child item, show the bucket menu
|
||||
this.bucketContext.Popup(this.ui.bucketTree.Viewport().MapToGlobal(pos))
|
||||
|
||||
} else {
|
||||
// Top-level item, show the database menu
|
||||
this.databaseContext.Popup(this.ui.bucketTree.Viewport().MapToGlobal(pos))
|
||||
}
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_actionRefresh_buckets_triggered() {
|
||||
this.refreshBucketTree(this.lastContextSelection)
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_bucketTree_currentItemChanged(current, previous *qt.QTreeWidgetItem) {
|
||||
_ = previous // Q_UNUSED
|
||||
if current == nil {
|
||||
this.ui.stackedWidget.SetVisible(false)
|
||||
return
|
||||
}
|
||||
|
||||
this.ui.stackedWidget.SetVisible(true)
|
||||
|
||||
if current.Parent() == nil {
|
||||
// Selected a database
|
||||
this.ui.stackedWidget.SetCurrentWidget(this.ui.databasePage)
|
||||
this.ui.databasePropertiesArea.Clear()
|
||||
|
||||
bdb := GET_BDB(current)
|
||||
stats, err := Bolt_DBStats(bdb)
|
||||
if err != nil {
|
||||
this.ui.databasePropertiesArea.SetPlainText(fmt.Sprintf("Error retrieving database statistics: %s", err.Error()))
|
||||
} else {
|
||||
this.ui.databasePropertiesArea.SetPlainText(stats)
|
||||
}
|
||||
|
||||
// Clean up foreign areas
|
||||
this.ui.bucketPropertiesArea.Clear()
|
||||
this.ui.bucketData.Clear()
|
||||
|
||||
} else {
|
||||
// Selected a bucket
|
||||
|
||||
this.ui.stackedWidget.SetCurrentWidget(this.ui.bucketPage)
|
||||
this.ui.bucketPropertiesArea.Clear()
|
||||
|
||||
ws := this.getSelection(current)
|
||||
|
||||
stats, err := Bolt_BucketStats(ws.bdb, ws.browse)
|
||||
if err != nil {
|
||||
this.ui.bucketPropertiesArea.SetPlainText(fmt.Sprintf("Error retrieving bucket statistics: %s", err.Error()))
|
||||
} else {
|
||||
this.ui.bucketPropertiesArea.SetPlainText(stats)
|
||||
}
|
||||
|
||||
// Load the data tab
|
||||
this.refreshData(ws.bdb, ws.browse)
|
||||
|
||||
// Clean up foreign areas
|
||||
this.ui.databasePropertiesArea.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
func (this *MainWindow) refreshData(bdb *bolt.DB, browse []string) {
|
||||
// Load the data tab
|
||||
this.ui.bucketData.Clear()
|
||||
|
||||
err := Bolt_ListItems(bdb, browse, func(lii ListItemInfo) error {
|
||||
itm := qt.NewQTreeWidgetItem()
|
||||
itm.SetText(0, getDisplayName(lii.Name))
|
||||
itm.SetData(0, BinaryDataRole, qt.NewQVariant15(MakeQByteArray(lii.Name)))
|
||||
itm.SetText(1, fmt.Sprintf("%d", lii.DataLen))
|
||||
this.ui.bucketData.AddTopLevelItem(itm)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
this.alert(fmt.Sprintf("Error listing bucket content: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
this.ui.bucketData.ResizeColumnToContents(0)
|
||||
this.on_bucketData_itemSelectionChanged()
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_actionClear_selection_triggered() {
|
||||
this.ui.bucketTree.SetCurrentItem(nil)
|
||||
}
|
||||
|
||||
type windowSelection struct {
|
||||
itm *qt.QTreeWidgetItem
|
||||
top *qt.QTreeWidgetItem
|
||||
browse []string
|
||||
bdb *bolt.DB
|
||||
}
|
||||
|
||||
func (this *MainWindow) getWindowSelection() (windowSelection, bool) {
|
||||
|
||||
itm := this.ui.bucketTree.CurrentItem()
|
||||
if itm == nil {
|
||||
return windowSelection{}, false // No selection
|
||||
}
|
||||
|
||||
return this.getSelection(itm), true
|
||||
}
|
||||
|
||||
func (this *MainWindow) getSelection(itm *qt.QTreeWidgetItem) windowSelection {
|
||||
|
||||
top := itm
|
||||
|
||||
var browse []string
|
||||
for {
|
||||
if top.Parent() == nil {
|
||||
break
|
||||
} else {
|
||||
browse = append(browse, FromQByteArray(top.Data(0, BinaryDataRole).ToByteArray()))
|
||||
top = top.Parent()
|
||||
}
|
||||
}
|
||||
|
||||
ReverseSlice(browse)
|
||||
|
||||
bdb := GET_BDB(top)
|
||||
|
||||
return windowSelection{itm: itm, top: top, browse: browse, bdb: bdb}
|
||||
}
|
||||
|
||||
func (this *MainWindow) openEditor(bdb *bolt.DB, saveAs []string, saveAsKey string, currentContent []byte) {
|
||||
iw := NewItemWindowUi()
|
||||
|
||||
iw.contentArea.SetPlainText(string(currentContent))
|
||||
iw.ItemWindow.SetWindowTitle(getDisplayName(saveAsKey))
|
||||
iw.ItemWindow.SetWindowModality(qt.ApplicationModal) // we need this - otherwise we'll refresh a possibly-changed area after saving
|
||||
iw.ItemWindow.OnFinished(func(exitCode int) {
|
||||
if exitCode == int(qt.QDialog__Accepted) {
|
||||
|
||||
err := Bolt_SetItem(bdb, saveAs, saveAsKey, iw.contentArea.ToPlainText())
|
||||
if err != nil {
|
||||
this.alert(fmt.Sprintf("Error saving item content: %s", err.Error()))
|
||||
}
|
||||
|
||||
this.refreshData(bdb, saveAs)
|
||||
}
|
||||
iw.ItemWindow.DeleteLater()
|
||||
})
|
||||
|
||||
iw.ItemWindow.Show()
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_bucketData_doubleClicked(index *qt.QModelIndex) {
|
||||
ws, ok := this.getWindowSelection()
|
||||
if !ok {
|
||||
return // no selection
|
||||
}
|
||||
|
||||
// Get item key
|
||||
|
||||
model := index.Model()
|
||||
key := FromQByteArray(model.Data2(model.Index(index.Row(), 0), BinaryDataRole).ToByteArray())
|
||||
|
||||
// DB lookup
|
||||
content, err := Bolt_GetItem(ws.bdb, ws.browse, key)
|
||||
if err != nil {
|
||||
this.alert(fmt.Sprintf("Error loading item content: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
this.openEditor(ws.bdb, ws.browse, key, []byte(content))
|
||||
}
|
||||
|
||||
func (this *MainWindow) on_actionAdd_bucket_triggered() {
|
||||
ws, ok := this.getWindowSelection()
|
||||
if !ok {
|
||||
return // no selection
|
||||
}
|
||||
|
||||
// Prompt for bucket name
|
||||
|
||||
name := qt.QInputDialog_GetText(this.Widget(), "New bucket", "Enter a key for the new bucket:")
|
||||
if len(name) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Create
|
||||
err := Bolt_CreateBucket(ws.bdb, ws.browse, name)
|
||||
if err != nil {
|
||||
this.alert(fmt.Sprintf("Error creating bucket: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh bucket list
|
||||
this.refreshBucketTree(ws.itm) // sub-tree only
|
||||