105 Commits

Author SHA1 Message Date
4e13d8dffd doc/README: changelog for v1.1.0 2025-05-04 14:45:50 +12:00
20e5efa711 makefile: set version to 1.1.0 2025-05-04 14:45:42 +12:00
6378740051 bump bbolt to v1.4.0 2025-05-04 14:43:03 +12:00
b8ce7a667b makefile: use miqt-docker for windows build 2025-05-04 14:25:22 +12:00
281ca18d90 uic: update to latest miqt-uic 2025-05-04 14:23:14 +12:00
1725de6ace import/export: use background thread 2025-04-18 12:13:56 +12:00
0ad7c03db0 makefile: add workaround for nested menu generation 2025-04-17 21:53:04 +12:00
1cdd0d113b support drag+drop database files in 2025-04-17 21:48:52 +12:00
a0fae43690 makefile/uic: work around miqt issue with SetObjectName 2025-04-17 21:27:06 +12:00
04ac766125 windows build: update build command for latest miqt 2025-04-17 21:26:51 +12:00
9ac26467c0 add zip roundtrip conversion feature 2025-04-17 21:26:27 +12:00
cbfc038839 add f1/f5 keyboard shortcuts for help/refresh 2025-04-17 21:25:38 +12:00
7ae6462da0 qbolt: do more work in []byte space instead of strings 2025-04-17 21:25:02 +12:00
09a3e5b90f main: force highdpi attributes (helps on windows) 2025-04-16 18:37:39 +12:00
1cddd17017 make(linux): smaller output binary 2025-04-16 18:33:42 +12:00
8fdc3a0428 makefile: allow overriding $GO 2025-04-16 18:17:52 +12:00
18f10fc1b4 update to miqt 0.10, update to qt 6 2025-04-16 18:17:44 +12:00
18331ae007 doc/README: updates for v1.0.3 2024-10-05 17:44:27 +13:00
f08242e93c Merge branch 'qbolt-go' 2024-10-05 17:42:14 +13:00
23965230e2 make: add windows resources, improve output compression 2024-10-05 17:41:38 +13:00
e43b261752 build: upgrade makefile/dockerfile for miqt and win64 support 2024-10-05 16:37:23 +13:00
f9b4cb71a5 qbolt: show makefile-based version number in help dialog 2024-10-05 16:37:15 +13:00
96c05641bd mod: upgrade bbolt v1.3.5 -> v1.3.11 2024-10-05 16:36:41 +13:00
2c5d1946ec qbolt: fixes post-port 2024-10-05 15:58:56 +13:00
a6cbc5a9ed qbolt: initial port to go/miqt 2024-10-03 19:34:28 +13:00
6008ae44a2 doc: rename image directory for teafolio 2021-04-12 18:03:05 +12:00
99096b2360 doc/README: add v1.0.2 changelog 2021-04-12 18:01:46 +12:00
0e92459779 no-op merge with legacy-codesite branch 2021-04-12 17:52:00 +12:00
a13eaaf523 make: upx doesn't support current mxe mingw/qt linked binaries 2021-04-12 17:44:38 +12:00
dca8d277d3 make: option to build windows binary in docker 2021-04-12 17:44:17 +12:00
15c739c0b9 squash 2021-04-12 17:43:59 +12:00
fc2da972ef dummy-data: fix build for renamed bbolt package 2021-04-12 17:07:16 +12:00
e45eea7111 makefile: remove 'hg export' function 2021-04-12 17:04:40 +12:00
1e62d79c07 doc: delete TODO.txt (issues now being tracked in Gitea)
The new issue tracker is at https://git.ivysaur.me/code.ivysaur.me/qbolt/issues
2021-04-12 17:04:27 +12:00
c74c5ae5c0 doc: move README/TODO to top-level directory 2021-04-12 17:00:49 +12:00
b0092c4a6e vendor: switch bolt implementation to etcd-io/bbolt v1.3.5 2021-04-12 17:00:40 +12:00
5ce6368c4a qt: fix missing -lpthread on Debian Bullseye 2021-04-12 16:54:06 +12:00
3e7e54da4b go: add go.mod file for bolt dependency 2021-04-12 16:53:57 +12:00
9f80d687b2 hg2git: replace hgignore/hgtags files 2021-04-12 16:53:43 +12:00
96411e877f bump all versions to 1.0.2 2017-06-19 21:04:40 +12:00
ac7e078c02 Added tag release-1.0.1 for changeset 0528a0ab20b6 2017-06-19 21:04:23 +12:00
e13314f5dc 1.0.1 meta 2017-06-19 21:04:17 +12:00
b50c3e738a doc: update features list in readme 2017-06-19 21:02:42 +12:00
54ad6015b7 more binary correctness 2017-06-19 20:53:50 +12:00
40e84ac230 dummy-data: change to generate binary names 2017-06-19 20:45:26 +12:00
767eaa0a47 add fallback display for non-printable characters 2017-06-19 20:42:51 +12:00
bb594e768c doc: update readme 2017-06-19 20:42:42 +12:00
516bd99c4d doc: update TODO 2017-06-19 20:28:33 +12:00
571bfcf4b6 one more preservation for previous 2017-06-19 20:27:52 +12:00
26f7a11d80 preserve the binary content of keys and bucket names during edit operations 2017-06-19 20:27:05 +12:00
21588021d3 doc: update TODO 2017-05-25 20:00:10 +12:00
7441e0c15b option to open database as read-only 2017-05-25 19:59:53 +12:00
142f3f6bf4 track screenshot of qbolt on windows 2017-05-25 19:54:35 +12:00
19ddb1c956 select parent when deleting bucket (maybe fixes a crash?) 2017-05-25 19:53:35 +12:00
97467eae4d add icon for win32 binary 2017-05-25 19:50:11 +12:00
17c37b6568 bump dist version to 1.0.1 2017-05-21 18:23:41 +12:00
57237ef2e8 Added tag release-1.0.0 for changeset 74cacbbe8f6c 2017-05-21 18:23:29 +12:00
164f5a071d doc: update README 2017-05-21 18:22:23 +12:00
ac46b16d0e hgignore: simplify 2017-05-21 18:19:47 +12:00
b9d114a2d8 remove extra .db file from dist archives 2017-05-21 18:18:45 +12:00
b368457247 add some screenshots 2017-05-21 18:18:14 +12:00
fb940d5f60 doc: TODO.txt 2017-05-21 18:18:10 +12:00
6d1e671e44 doc: README 2017-05-21 18:18:04 +12:00
b5149c4efa bolt: strict 10 second timeout for opening databases 2017-05-21 18:05:20 +12:00
f0f642b6b0 add many more icons 2017-05-21 18:05:11 +12:00
7c62abb352 working add items 2017-05-21 17:59:27 +12:00
4f1c48b55d working edit/delete items 2017-05-21 17:53:40 +12:00
37f9307db1 wip set/delete items 2017-05-21 17:44:07 +12:00
14e7ebb59c go: simplify out a wrapper function 2017-05-21 17:26:09 +12:00
14510545d4 fix right-hand default pane when a bucket is selected 2017-05-21 17:14:32 +12:00
906cff2e57 working bucket deletions 2017-05-21 17:14:20 +12:00
bf28495b1f add our application icon to system popups 2017-05-21 17:12:02 +12:00
98b78ab1e6 option to create new buckets / sub-buckets 2017-05-21 17:08:15 +12:00
4394c8a50a option to create new databases 2017-05-21 16:50:17 +12:00
9fb5bdad78 always open databases read/write 2017-05-21 16:47:28 +12:00
d2ec12798e popup editor to view record content 2017-05-21 16:44:44 +12:00
5270dd00bc display data keys, data item length 2017-05-21 16:15:49 +12:00
35f28fa5ed makefile: fix qbolt.a target 2017-05-21 15:59:00 +12:00
5ea969e10c pretty-print json display 2017-05-21 15:49:47 +12:00
d0becd0c3c clean up qt project, always refer to makefile-produced qbolt.a files 2017-05-21 15:00:53 +12:00
1c81444645 makefile: distribution targets 2017-05-21 14:51:22 +12:00
d7c3bfd1f5 unify makefile, make win32 builds 2017-05-21 14:33:46 +12:00
0f1cc014d7 working 'refresh buckets' action 2017-05-21 13:49:41 +12:00
6ac8c3e67b remove dead code 2017-05-21 13:49:36 +12:00
546681a0ef working cgo slices, working nested-bucket operations 2017-05-21 13:47:46 +12:00
679d1140cc recursive bucket scan - we're crashing when passing a slice to go code 2017-05-21 13:31:28 +12:00
60d71104e8 retrieve bucket properties 2017-05-21 12:39:55 +12:00
95a1bfeea5 retrieve database properties 2017-05-21 11:59:38 +12:00
8597f270f6 context menus, disconnection, re-refresh, selection behaviour, ^O shortcut 2017-05-21 11:47:16 +12:00
d32ab82d73 don't pass go strings to c - pass char*+len, callee must call free() 2017-05-20 23:02:58 +12:00
b6d1cafe54 cgo: minor cleanups, remove unused export 2017-05-20 18:15:39 +12:00
3911d7d66c convert cgo function pointer call to repeated iteration 2017-05-20 18:01:55 +12:00
be594cb1a5 call bucket enumeration - function pointers are misbehaving 2017-05-20 17:28:27 +12:00
8a93f163c4 c++ wrappers for bucket enumeration function 2017-05-20 15:47:18 +12:00
e82d0a5dd2 add top-level db items to ui 2017-05-20 15:47:05 +12:00
4b3fb783c4 wire up other menu items, add app logo icon 2017-05-20 15:20:30 +12:00
e8030a9579 cgo: change interface to not return already-collected error interfaces 2017-05-20 15:13:16 +12:00
a94a330345 add GetMagic() check, move c/go interop into separate classes 2017-05-20 14:57:51 +12:00
8ce03cbed6 go: build .a instead of .so 2017-05-20 14:57:19 +12:00
b76fad9512 hgignore: pro.user file 2017-05-18 20:11:19 +12:00
a9158c3a0c gms: fix missing initialisation 2017-05-16 19:47:50 +12:00
dc31787526 track sample.db file 2017-05-16 19:47:41 +12:00
ff7484079e track dummy-data generator 2017-05-16 19:47:26 +12:00
6e1f9ca1e1 hgignore 2017-05-16 19:47:17 +12:00
ac56f5e6c8 initial commit 2017-05-16 19:34:54 +12:00
44 changed files with 2366 additions and 21 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# development
qbolt
dummy-data/dummy-data*
# 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-*

View File

@@ -1,9 +0,0 @@
# Converted with codesite2git
project_name="qbolt"
short_description="A graphical database manager for BoltDB."
written_in_lang="C++ (Qt), Golang (CGo)"
topics=[]
ctime=1495324800
mtime=1497830400

67
Makefile Normal file
View File

@@ -0,0 +1,67 @@
VERSION := 1.1.0
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
GO := go
MIQT_DOCKER := miqt-docker
MIQT_UIC := miqt-uic
MIQT_RCC := miqt-rcc
GO_WINRES := go-winres
SOURCES := $(wildcard *.go *.ui *.qrc) resources.go resources.rcc mainwindow_ui.go itemwindow_ui.go rsrc_windows_amd64.syso
.PHONY: all
all: build/qbolt build/qbolt.exe
.PHONY: dist
dist: build/qbolt-${VERSION}-windows-x86_64.zip build/qbolt-${VERSION}-debian12-x86_64.tar.xz
.PHONY: clean
clean:
rm -f qbolt
rm -rf build
mkdir -p build
touch build/.create_dir
rm -f windows-manifest.json
rm -f rsrc_windows_amd64.syso
rm -f resources.go
rm -f resources.rcc
# Generated files
resources.rcc resources.go: resources.qrc
$(MIQT_RCC) -Qt6 -Input resources.qrc
mainwindow_ui.go: mainwindow.ui
$(MIQT_UIC) -Qt6 -InFile mainwindow.ui -OutFile mainwindow_ui.go
itemwindow_ui.go: itemwindow.ui
$(MIQT_UIC) -Qt6 -InFile itemwindow.ui -OutFile itemwindow_ui.go
windows-manifest.json: windows-manifest.template.json Makefile
cat windows-manifest.template.json | sed -re 's_%VERSION%_$(VERSION)_' > windows-manifest.json
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
# Linux release
build/qbolt: $(SOURCES)
CGO_CFLAGS='-Os -ffunction-sections -fdata-sections -flto=auto' CGO_CXXFLAGS='-Os -ffunction-sections -fdata-sections -flto=auto' CGO_LDFLAGS='-Wl,--gc-sections -flto=auto -fwhole-program' $(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)
# -flto causes internal compiler error
$(MIQT_DOCKER) win64-qt6-static /bin/bash -c "CGO_CFLAGS='-Os -ffunction-sections -fdata-sections' CGO_CXXFLAGS='-Os -ffunction-sections -fdata-sections' CGO_LDFLAGS='-Wl,--gc-sections -fwhole-program' go build $(GOFLAGS_W) -o build/qbolt.exe"
# Must be stripped before upx'ing - @ref https://github.com/msys2/MSYS2-packages/issues/454
# However this removes the rsrc, loses the icon and causes a Defender detection
# strip build/qbolt.exe
# upx 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

View File

@@ -1,12 +1,10 @@
# qbolt
![](https://img.shields.io/badge/written%20in-C%2B%2B%20%28Qt%29%2C%20Golang%20%28CGo%29-blue)
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 Golang (Qt)
## Features
@@ -24,25 +22,41 @@ Source code content of `qbolt-x.x.x-src.tar.gz` is released under the ISC licens
BoltDB is released under the MIT license.
The Windows binary is released under LGPL-3+ owing to the static copy of Qt.
## See Also
## See also
- BoltDB https://github.com/boltdb/bolt
## Changelog
2025-05-04 1.1.0
- New feature to import/export database as zip archive
- Upgrade to Qt 6
- Add keyboard shortcuts for refresh
- Improve High DPI support
- Rebuild artefacts with miqt v0.10.0, etcd-io/bbolt v1.4.0, go 1.23, Qt 6.8 (win64)
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.1.0)
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
- Use Go modules
- Add support for building Windows binary in Docker
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.2)
2017-06-19 1.0.1
- Feature: Option to open database as read-only
- Fix an issue with support for bucket names and keys not surviving UTF-8 roundtrips (now binary-clean)
- Fix an issue with crashing when deleting a bucket other than the selected one
- Fix a cosmetic issue with application icon on Windows
- [ qbolt-1.0.1-win32.zip](dist-archive/qbolt-1.0.1-win32.zip) *(5.07 MiB)*
- [⬇️ qbolt-1.0.1-src.tar.gz](dist-archive/qbolt-1.0.1-src.tar.gz) *(265.88 KiB)*
- [⬇️ qbolt-1.0.1-linux_amd64.tar.xz](dist-archive/qbolt-1.0.1-linux_amd64.tar.xz) *(584.88 KiB)*
- [ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.1)
2017-05-21 1.0.0
- Initial public release
- [⬇️ qbolt-1.0.0-win32.zip](dist-archive/qbolt-1.0.0-win32.zip) *(5.07 MiB)*
- [ qbolt-1.0.0-src.tar.gz](dist-archive/qbolt-1.0.0-src.tar.gz) *(221.91 KiB)*
- [⬇️ qbolt-1.0.0-linux_amd64.tar.xz](dist-archive/qbolt-1.0.0-linux_amd64.tar.xz) *(584.62 KiB)*
- 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)

223
bolt.go Normal file
View File

@@ -0,0 +1,223 @@
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 []byte) 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(key, val)
})
}
func Bolt_DeleteItem(db *bolt.DB, browse []string, key []byte) 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(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) error {
if len(browse) == 0 {
// root mode
return db.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(k []byte, _ *bolt.Bucket) error {
return cb(string(k))
})
})
}
// 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 {
return cb(string(k))
}
return nil
})
})
}
type ListItemInfo struct {
Name []byte
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
}
kcopy := make([]byte, len(k))
copy(kcopy, k)
return cb(ListItemInfo{kcopy, int64(len(v))})
})
})
}
func Bolt_GetItem(db *bolt.DB, browse []string, key []byte) ([]byte, error) {
var ret []byte
err := withBrowse_ReadOnly(db, browse, func(tx *bolt.Tx, bucket *bolt.Bucket) error {
d := bucket.Get([]byte(key))
ret = make([]byte, len(d))
copy(ret, d)
return nil
})
return ret, err
}
func Bolt_Close(db *bolt.DB) error {
return db.Close()
}

0
build/.create_dir Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

82
dummy-data/main.go Normal file
View File

@@ -0,0 +1,82 @@
package main
import (
"math/rand"
"os"
bolt "go.etcd.io/bbolt"
)
func random_name() string {
ret := make([]byte, 12)
rand.Read(ret)
return string(ret)
//return fmt.Sprintf("%08x-%08x-%08x", rand.Int63(), rand.Int63(), rand.Int63())
}
func fill_bucket(tx *bolt.Tx, bucket *bolt.Bucket) error {
// fill with some basic items
for i := 0; i < 30; i += 1 {
err := bucket.Put([]byte(random_name()), []byte("SAMPLE CONTENT "+random_name()))
if err != nil {
return err
}
}
// 1/20 (5%) chance of recursion
if rand.Intn(100) >= 95 {
for i := 0; i < 5; i += 1 {
child, err := bucket.CreateBucket([]byte(random_name()))
if err != nil {
return err
}
err = fill_bucket(tx, child)
if err != nil {
return err
}
}
}
return nil
}
func main() {
db, err := bolt.Open("sample.db", 0644, bolt.DefaultOptions)
if err != nil {
panic(err)
}
err = db.Update(func(tx *bolt.Tx) error {
// top-level buckets
for i := 0; i < 50; i += 1 {
bucketName := random_name()
bucket, err := tx.CreateBucket([]byte(bucketName))
if err != nil {
return err
}
err = fill_bucket(tx, bucket)
if err != nil {
return err
}
}
return nil
})
if err != nil {
panic(err)
}
err = db.Close()
if err != nil {
panic(err)
}
os.Exit(0)
}

167
export.go Normal file
View File

@@ -0,0 +1,167 @@
package main
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"
)
func Bolt_ExportDatabaseToZip(dbpath, zippath string) error {
db, err := Bolt_Open(true, dbpath)
if err != nil {
return fmt.Errorf("Error opening database: %w", err)
}
defer db.Close()
fh, err := os.OpenFile(zippath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("Error opening output file: %w", err)
}
defer fh.Close()
zw := zip.NewWriter(fh)
// Filenames in zip files cannot contain `/` characters. Mangle it
safename := func(n string) string {
return strings.ReplaceAll(string(n), `/`, `__`)
}
var process func(currentPath []string) error
process = func(currentPath []string) error {
return Bolt_ListBuckets(db, currentPath, func(bucket string) error {
// Create entry for our own bucket
ourBucket := zip.FileHeader{
Name: path.Join(path.Join(Apply(currentPath, safename)...), safename(bucket)) + `/`, // Trailing slash = directory
}
ourBucket.SetMode(fs.ModeDir | 0755)
_, err := zw.CreateHeader(&ourBucket)
if err != nil {
return err
}
// Child pathspec
childPath := CopySliceAdd(currentPath, bucket)
// Create file entries for all non-bucket children
err = Bolt_ListItems(db, childPath, func(li ListItemInfo) error {
fileItem := zip.FileHeader{
Name: path.Join(path.Join(Apply(childPath, safename)...), safename(string(li.Name))),
}
fileItem.SetMode(0644)
fileW, err := zw.CreateHeader(&fileItem)
if err != nil {
return err
}
buff, err := Bolt_GetItem(db, childPath, []byte(li.Name))
if err != nil {
return err
}
_, err = io.CopyN(fileW, bytes.NewReader(buff), li.DataLen)
return err
})
if err != nil {
return err
}
// Recurse for all bucket-type children
process(childPath)
// Done
return nil
})
}
err = process([]string{})
if err != nil {
return err
}
err = zw.Flush()
if err != nil {
return err
}
err = zw.Close()
if err != nil {
return err
}
return fh.Close()
}
func Bolt_ImportZipToDatabase(dbpath, zippath string) error {
db, err := Bolt_Open(false, dbpath)
if err != nil {
return fmt.Errorf("Error opening target database: %w", err)
}
defer db.Close()
fh, err := os.OpenFile(zippath, os.O_RDONLY, 0400)
if err != nil {
return fmt.Errorf("Error opening input archive: %w", err)
}
defer fh.Close()
fstat, err := fh.Stat()
if err != nil {
return err
}
zr, err := zip.NewReader(fh, fstat.Size())
if err != nil {
return fmt.Errorf("Reading zip file format: %w", err)
}
for _, zf := range zr.File {
if strings.HasSuffix(zf.Name, `/`) || (zf.Mode()&fs.ModeDir) != 0 {
// Bucket
bucketPath := strings.Split(strings.TrimSuffix(zf.Name, `/`), `/`)
err = Bolt_CreateBucket(db, bucketPath[0:len(bucketPath)-1], bucketPath[len(bucketPath)-1])
if err != nil {
return fmt.Errorf("Creating bucket %q: %w", zf.Name, err)
}
} else {
// Object
objectPath := strings.Split(zf.Name, `/`)
rc, err := zf.Open()
if err != nil {
return err
}
content, err := io.ReadAll(rc)
if err != nil {
return err
}
err = Bolt_SetItem(db, objectPath[0:len(objectPath)-1], []byte(objectPath[len(objectPath)-1]), content)
if err != nil {
return err
}
err = rc.Close()
if err != nil {
return err
}
}
}
// Done
return nil
}

12
go.mod Normal file
View File

@@ -0,0 +1,12 @@
module code.ivysaur.me/qbolt
go 1.23.0
toolchain go1.23.3
require (
github.com/mappu/miqt v0.10.0
go.etcd.io/bbolt v1.4.0
)
require golang.org/x/sys v0.32.0 // indirect

20
go.sum Normal file
View File

@@ -0,0 +1,20 @@
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/mappu/miqt v0.10.0 h1:w+ucRwdoIO7xS32us34lL2Mh0+aarywNpQz6c76ZSDY=
github.com/mappu/miqt v0.10.0/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U=
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/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

95
itemwindow.ui Normal file
View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ItemWindow</class>
<widget class="QDialog" name="ItemWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>370</width>
<height>353</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/database_lightning.png</normaloff>:/rsrc/database_lightning.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QPlainTextEdit" name="contentArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QFrame" name="frame">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ItemWindow</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>184</x>
<y>330</y>
</hint>
<hint type="destinationlabel">
<x>184</x>
<y>176</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ItemWindow</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>184</x>
<y>330</y>
</hint>
<hint type="destinationlabel">
<x>184</x>
<y>176</y>
</hint>
</hints>
</connection>
</connections>
</ui>

77
itemwindow_ui.go Normal file
View File

@@ -0,0 +1,77 @@
// Generated by miqt-uic. To update this file, edit the .ui file in
// Qt Designer, and then run 'go generate'.
//
//go:generate miqt-uic -Qt6 -InFile itemwindow.ui -OutFile itemwindow_ui.go
package main
import (
qt "github.com/mappu/miqt/qt6"
)
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.NewQDialog(nil)
ItemWindow__objectName := qt.NewQAnyStringView3("ItemWindow")
ui.ItemWindow.SetObjectName(*ItemWindow__objectName)
ItemWindow__objectName.Delete() // setter copied value
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)
gridLayout_2__objectName := qt.NewQAnyStringView3("gridLayout_2")
ui.gridLayout_2.SetObjectName(*gridLayout_2__objectName)
gridLayout_2__objectName.Delete() // setter copied value
ui.gridLayout_2.SetVerticalSpacing(0)
ui.gridLayout_2.SetContentsMargins(0, 0, 0, 0)
ui.gridLayout_2.SetSpacing(6)
ui.contentArea = qt.NewQPlainTextEdit(ui.ItemWindow.QWidget)
contentArea__objectName := qt.NewQAnyStringView3("contentArea")
ui.contentArea.SetObjectName(*contentArea__objectName)
contentArea__objectName.Delete() // setter copied value
ui.contentArea.SetFrameShape(qt.QFrame__NoFrame)
ui.gridLayout_2.AddWidget2(ui.contentArea.QWidget, 0, 0)
ui.frame = qt.NewQFrame(ui.ItemWindow.QWidget)
frame__objectName := qt.NewQAnyStringView3("frame")
ui.frame.SetObjectName(*frame__objectName)
frame__objectName.Delete() // setter copied value
ui.gridLayout = qt.NewQGridLayout(ui.frame.QWidget)
gridLayout__objectName := qt.NewQAnyStringView3("gridLayout")
ui.gridLayout.SetObjectName(*gridLayout__objectName)
gridLayout__objectName.Delete() // setter copied value
ui.gridLayout.SetContentsMargins(11, 11, 11, 11)
ui.gridLayout.SetSpacing(6)
ui.buttonBox = qt.NewQDialogButtonBox(ui.frame.QWidget)
buttonBox__objectName := qt.NewQAnyStringView3("buttonBox")
ui.buttonBox.SetObjectName(*buttonBox__objectName)
buttonBox__objectName.Delete() // setter copied value
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() {
}

28
main.go Normal file
View File

@@ -0,0 +1,28 @@
package main
import (
"os"
qt "github.com/mappu/miqt/qt6"
)
var Version string = "v0.0.0-devel"
func main() {
_ = qt.NewQApplication(os.Args)
qt.QGuiApplication_SetApplicationDisplayName("QBolt")
qt.QGuiApplication_SetWindowIcon(qt.NewQIcon4(":/rsrc/database_lightning.png"))
// High DPI tweaks
qt.QCoreApplication_SetAttribute2(qt.AA_EnableHighDpiScaling, true)
qt.QCoreApplication_SetAttribute2(qt.AA_UseHighDpiPixmaps, true)
qt.QCoreApplication_SetAttribute2(qt.AA_Use96Dpi, true)
qt.QGuiApplication_SetHighDpiScaleFactorRoundingPolicy(qt.PassThrough)
w := NewMainWindow()
w.ui.MainWindow.Show()
qt.QApplication_Exec()
}

586
mainwindow.go Normal file
View File

@@ -0,0 +1,586 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
qt "github.com/mappu/miqt/qt6"
"github.com/mappu/miqt/qt6/mainthread"
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.ui.MainWindow.QWidget)
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.ui.MainWindow.QWidget)
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.actionExport_database_as_zip.OnTriggered(this.onactionExport_database_as_zip_triggered)
this.ui.actionCreate_database_from_zip.OnTriggered(this.onactionCreate_database_from_zip_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)
this.ui.MainWindow.SetAcceptDrops(true)
this.ui.MainWindow.OnDragEnterEvent(this.onDragEnter)
this.ui.MainWindow.OnDragMoveEvent(this.onDragMove)
this.ui.MainWindow.OnDropEvent(this.onDropEvent)
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.NewQVariant4(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) onDragEnter(super func(event *qt.QDragEnterEvent), event *qt.QDragEnterEvent) {
if !event.MimeData().HasUrls() {
event.Ignore()
return
}
event.Accept()
}
func (this *MainWindow) onDragMove(super func(event *qt.QDragMoveEvent), event *qt.QDragMoveEvent) {
if !event.MimeData().HasUrls() {
event.Ignore()
return
}
event.SetDropAction(qt.CopyAction)
event.Accept()
}
func (this *MainWindow) onDropEvent(super func(event *qt.QDropEvent), event *qt.QDropEvent) {
if !event.MimeData().HasUrls() {
event.Ignore()
return
}
event.Accept()
paths := event.MimeData().Urls()
for _, path := range paths {
lpath := path.ToLocalFile()
this.openDatabase(lpath, false)
}
}
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 (this *MainWindow) onactionExport_database_as_zip_triggered() {
dbPath := qt.QFileDialog_GetOpenFileName2(this.Widget(), "Select bolt database...")
if dbPath == "" {
return
}
zipPath := qt.QFileDialog_GetSaveFileName4(this.Widget(), "Save as...", "", "Zip files (*.zip)")
if zipPath == "" {
return
}
go func() {
err := Bolt_ExportDatabaseToZip(dbPath, zipPath)
mainthread.Start(func() {
if err != nil {
this.alert(fmt.Sprintf("Error exporting database as zip: %s", err.Error()))
return
}
this.alert("Exported as zip successfully.")
})
}()
}
func (this *MainWindow) onactionCreate_database_from_zip_triggered() {
zipPath := qt.QFileDialog_GetOpenFileName4(this.Widget(), "Select zip archive...", "", "Zip files (*.zip)")
if zipPath == "" {
return
}
dbPath := qt.QFileDialog_GetSaveFileName2(this.Widget(), "Save as...")
if dbPath == "" {
return
}
// Qt popped up a message saying 'will overwrite existing'
// Make that true
if err := os.Remove(dbPath); err != nil && !os.IsNotExist(err) {
this.alert(fmt.Sprintf("Error removing existing database for overwrite: %s", err.Error()))
return
}
go func() {
err := Bolt_ImportZipToDatabase(dbPath, zipPath)
mainthread.Start(func() {
if err != nil {
this.alert(fmt.Sprintf("Error importing database from zip %q: %s", zipPath, err.Error()))
return
}
this.alert("Imported zip to database successfully.")
})
}()
}
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) error {
child := qt.NewQTreeWidgetItem6(itm) // NewQTreeWidgetItem()
child.SetText(0, getDisplayName([]byte(qba)))
child.SetData(0, BinaryDataRole, qt.NewQVariant12([]byte(qba)))
child.SetIcon(0, qt.NewQIcon4(":/rsrc/table.png"))
itm.AddChild(child)
this.refreshBucketTree(child)
return nil
})
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 &quot;Silk&quot; 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().MapToGlobalWithQPoint(pos))
} else {
// Top-level item, show the database menu
this.databaseContext.Popup(this.ui.bucketTree.Viewport().MapToGlobalWithQPoint(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.NewQVariant12([]byte(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, string(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 []byte, 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, []byte(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
key := index.DataWithRole(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
this.ui.bucketTree.ExpandItem(ws.itm)
}
func (this *MainWindow) on_actionDelete_bucket_triggered() {
ws, ok := this.getWindowSelection()
if !ok {
return // no selection
}
// Prompt for confirmation
bucketToDelete := ws.itm.Data(0, BinaryDataRole).ToByteArray()
if qt.QMessageBox_Question2(
this.Widget(),
"Delete bucket",
fmt.Sprintf("Are you sure you want to remove the bucket '%s'?", getDisplayName(bucketToDelete)),
qt.QMessageBox__Yes,
qt.QMessageBox__Cancel,
) != int(qt.QMessageBox__Yes) {
return
}
parent := ws.itm.Parent()
// One level down
if len(ws.browse) > 0 {
ws.browse = ws.browse[0 : len(ws.browse)-1]
}
err := Bolt_DeleteBucket(ws.bdb, ws.browse, string(bucketToDelete))
if err != nil {
this.alert(fmt.Sprintf("Error removing bucket: %s", err.Error()))
return
}
// Refresh bucket list
this.refreshBucketTree(parent) // sub-tree only
this.ui.bucketTree.ExpandItem(parent)
this.ui.bucketTree.SetCurrentItem(parent)
}
func (this *MainWindow) on_AddDataButton_clicked() {
ws, ok := this.getWindowSelection()
if !ok {
return // no selection
}
// Prompt for bucket name
name := qt.QInputDialog_GetText(this.Widget(), "New item", "Enter a key for the new item:")
if len(name) == 0 {
return
}
this.openEditor(ws.bdb, ws.browse, []byte(name), []byte(""))
}
func (this *MainWindow) on_DeleteDataButton_clicked() {
ws, ok := this.getWindowSelection()
if !ok {
return // no selection
}
selection := this.ui.bucketData.SelectedItems()
if len(selection) == 0 {
return // nothing to do
}
// Prompt for confirmation
if qt.QMessageBox_Question2(this.Widget(), "Delete items", fmt.Sprintf("Are you sure you want to remove %d item(s)?", len(selection)), qt.QMessageBox__Yes, qt.QMessageBox__Cancel) != int(qt.QMessageBox__Yes) {
return
}
var i int = len(selection)
for i > 0 {
err := Bolt_DeleteItem(ws.bdb, ws.browse, selection[i-1].Data(0, BinaryDataRole).ToByteArray())
if err != nil {
this.alert(fmt.Sprintf("Error removing item: %s", err.Error()))
return
}
i -= 1
}
this.refreshData(ws.bdb, ws.browse)
}
func (this *MainWindow) on_bucketData_itemSelectionChanged() {
this.ui.DeleteDataButton.SetEnabled(len(this.ui.bucketData.SelectedItems()) > 0)
}

436
mainwindow.ui Normal file
View File

@@ -0,0 +1,436 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>668</width>
<height>405</height>
</rect>
</property>
<property name="windowTitle">
<string>QBolt</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/database_lightning.png</normaloff>:/rsrc/database_lightning.png</iconset>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QTreeWidget" name="bucketTree">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<column>
<property name="text">
<string>Bucket</string>
</property>
</column>
</widget>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="databasePage">
<layout class="QGridLayout" name="gridLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QTabWidget" name="databaseTabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="databasePropertiesTab">
<attribute name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/chart_bar.png</normaloff>:/rsrc/chart_bar.png</iconset>
</attribute>
<attribute name="title">
<string>Database</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item row="0" column="0">
<widget class="QPlainTextEdit" name="databasePropertiesArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="plainText">
<string>No selection</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="bucketPage">
<layout class="QGridLayout" name="gridLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QTabWidget" name="bucketTabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="bucketPropertiesTab">
<attribute name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/chart_bar.png</normaloff>:/rsrc/chart_bar.png</iconset>
</attribute>
<attribute name="title">
<string>Bucket</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item row="0" column="0">
<widget class="QPlainTextEdit" name="bucketPropertiesArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="bucketDataTab">
<attribute name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/table.png</normaloff>:/rsrc/table.png</iconset>
</attribute>
<attribute name="title">
<string>Data</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_6">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item row="0" column="0" colspan="3">
<widget class="QTreeWidget" name="bucketData">
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="indentation">
<number>0</number>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Key</string>
</property>
</column>
<column>
<property name="text">
<string>Data length</string>
</property>
</column>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="AddDataButton">
<property name="text">
<string>Add...</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/add.png</normaloff>:/rsrc/add.png</iconset>
</property>
</widget>
</item>
<item row="1" column="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="DeleteDataButton">
<property name="text">
<string>Delete...</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/delete.png</normaloff>:/rsrc/delete.png</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>668</width>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>&amp;File</string>
</property>
<widget class="QMenu" name="menuConvert">
<property name="title">
<string>Convert</string>
</property>
<addaction name="actionExport_database_as_zip"/>
<addaction name="actionCreate_database_from_zip"/>
</widget>
<addaction name="actionNew_database"/>
<addaction name="actionOpen_database"/>
<addaction name="actionOpen_database_as_read_only"/>
<addaction name="separator"/>
<addaction name="menuConvert"/>
<addaction name="separator"/>
<addaction name="separator"/>
<addaction name="actionExit"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="actionAbout_qbolt"/>
<addaction name="actionAbout_Qt"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
<string>&amp;View</string>
</property>
<addaction name="actionClear_selection"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuView"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="mainToolBar">
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionNew_database"/>
<addaction name="actionOpen_database"/>
<addaction name="separator"/>
</widget>
<widget class="QStatusBar" name="statusBar"/>
<action name="actionAbout_qbolt">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/information.png</normaloff>:/rsrc/information.png</iconset>
</property>
<property name="text">
<string>&amp;About QBolt</string>
</property>
<property name="shortcut">
<string>F1</string>
</property>
</action>
<action name="actionAbout_Qt">
<property name="text">
<string>About &amp;Qt</string>
</property>
</action>
<action name="actionOpen_database">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/database.png</normaloff>:/rsrc/database.png</iconset>
</property>
<property name="text">
<string>&amp;Open database...</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionExit">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/door_out.png</normaloff>:/rsrc/door_out.png</iconset>
</property>
<property name="text">
<string>&amp;Exit</string>
</property>
</action>
<action name="actionDisconnect">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/disconnect.png</normaloff>:/rsrc/disconnect.png</iconset>
</property>
<property name="text">
<string>Disconnect</string>
</property>
</action>
<action name="actionDelete_bucket">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/table_delete.png</normaloff>:/rsrc/table_delete.png</iconset>
</property>
<property name="text">
<string>Delete bucket</string>
</property>
</action>
<action name="actionRefresh_buckets">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/arrow_refresh.png</normaloff>:/rsrc/arrow_refresh.png</iconset>
</property>
<property name="text">
<string>Refresh buckets</string>
</property>
<property name="shortcut">
<string>F5</string>
</property>
</action>
<action name="actionClear_selection">
<property name="text">
<string>&amp;Clear selection</string>
</property>
</action>
<action name="actionNew_database">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/database_add.png</normaloff>:/rsrc/database_add.png</iconset>
</property>
<property name="text">
<string>&amp;New database...</string>
</property>
</action>
<action name="actionAdd_bucket">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/table_add.png</normaloff>:/rsrc/table_add.png</iconset>
</property>
<property name="text">
<string>Add bucket...</string>
</property>
</action>
<action name="actionOpen_database_as_read_only">
<property name="text">
<string>Open database as read-only...</string>
</property>
</action>
<action name="actionExport_database_as_zip">
<property name="text">
<string>Export database as zip</string>
</property>
</action>
<action name="actionCreate_database_from_zip">
<property name="text">
<string>Create database from zip</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>
<include location="resources.qrc"/>
</resources>
<connections/>
</ui>

390
mainwindow_ui.go Normal file
View File

@@ -0,0 +1,390 @@
// Generated by miqt-uic. To update this file, edit the .ui file in
// Qt Designer, and then run 'go generate'.
//
//go:generate miqt-uic -Qt6 -InFile mainwindow.ui -OutFile mainwindow_ui.go
package main
import (
qt "github.com/mappu/miqt/qt6"
)
type MainWindowUi struct {
MainWindow *qt.QMainWindow
centralWidget *qt.QWidget
gridLayout *qt.QGridLayout
splitter *qt.QSplitter
bucketTree *qt.QTreeWidget
stackedWidget *qt.QStackedWidget
databasePage *qt.QWidget
gridLayout_4 *qt.QGridLayout
databaseTabWidget *qt.QTabWidget
databasePropertiesTab *qt.QWidget
gridLayout_2 *qt.QGridLayout
databasePropertiesArea *qt.QPlainTextEdit
bucketPage *qt.QWidget
gridLayout_3 *qt.QGridLayout
bucketTabWidget *qt.QTabWidget
bucketPropertiesTab *qt.QWidget
gridLayout_5 *qt.QGridLayout
bucketPropertiesArea *qt.QPlainTextEdit
bucketDataTab *qt.QWidget
gridLayout_6 *qt.QGridLayout
bucketData *qt.QTreeWidget
AddDataButton *qt.QPushButton
horizontalSpacer *qt.QSpacerItem
DeleteDataButton *qt.QPushButton
menuBar *qt.QMenuBar
menuFile *qt.QMenu
menuConvert *qt.QMenu
menuHelp *qt.QMenu
menuView *qt.QMenu
mainToolBar *qt.QToolBar
statusBar *qt.QStatusBar
actionAbout_qbolt *qt.QAction
actionAbout_Qt *qt.QAction
actionOpen_database *qt.QAction
actionExit *qt.QAction
actionDisconnect *qt.QAction
actionDelete_bucket *qt.QAction
actionRefresh_buckets *qt.QAction
actionClear_selection *qt.QAction
actionNew_database *qt.QAction
actionAdd_bucket *qt.QAction
actionOpen_database_as_read_only *qt.QAction
actionExport_database_as_zip *qt.QAction
actionCreate_database_from_zip *qt.QAction
}
// NewMainWindowUi creates all Qt widget classes for MainWindow.
func NewMainWindowUi() *MainWindowUi {
ui := &MainWindowUi{}
ui.MainWindow = qt.NewQMainWindow(nil)
MainWindow__objectName := qt.NewQAnyStringView3("MainWindow")
ui.MainWindow.SetObjectName(*MainWindow__objectName)
MainWindow__objectName.Delete() // setter copied value
ui.MainWindow.Resize(668, 405)
ui.MainWindow.SetWindowTitle("QBolt")
icon0 := qt.NewQIcon()
icon0.AddFile4(":/rsrc/database_lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.MainWindow.SetWindowIcon(icon0)
ui.actionAbout_qbolt = qt.NewQAction()
actionAbout_qbolt__objectName := qt.NewQAnyStringView3("actionAbout_qbolt")
ui.actionAbout_qbolt.SetObjectName(*actionAbout_qbolt__objectName)
actionAbout_qbolt__objectName.Delete() // setter copied value
icon1 := qt.NewQIcon()
icon1.AddFile4(":/rsrc/information.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionAbout_qbolt.SetIcon(icon1)
ui.actionAbout_Qt = qt.NewQAction()
actionAbout_Qt__objectName := qt.NewQAnyStringView3("actionAbout_Qt")
ui.actionAbout_Qt.SetObjectName(*actionAbout_Qt__objectName)
actionAbout_Qt__objectName.Delete() // setter copied value
ui.actionOpen_database = qt.NewQAction()
actionOpen_database__objectName := qt.NewQAnyStringView3("actionOpen_database")
ui.actionOpen_database.SetObjectName(*actionOpen_database__objectName)
actionOpen_database__objectName.Delete() // setter copied value
icon2 := qt.NewQIcon()
icon2.AddFile4(":/rsrc/database.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionOpen_database.SetIcon(icon2)
ui.actionExit = qt.NewQAction()
actionExit__objectName := qt.NewQAnyStringView3("actionExit")
ui.actionExit.SetObjectName(*actionExit__objectName)
actionExit__objectName.Delete() // setter copied value
icon3 := qt.NewQIcon()
icon3.AddFile4(":/rsrc/door_out.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionExit.SetIcon(icon3)
ui.actionDisconnect = qt.NewQAction()
actionDisconnect__objectName := qt.NewQAnyStringView3("actionDisconnect")
ui.actionDisconnect.SetObjectName(*actionDisconnect__objectName)
actionDisconnect__objectName.Delete() // setter copied value
icon4 := qt.NewQIcon()
icon4.AddFile4(":/rsrc/disconnect.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionDisconnect.SetIcon(icon4)
ui.actionDelete_bucket = qt.NewQAction()
actionDelete_bucket__objectName := qt.NewQAnyStringView3("actionDelete_bucket")
ui.actionDelete_bucket.SetObjectName(*actionDelete_bucket__objectName)
actionDelete_bucket__objectName.Delete() // setter copied value
icon5 := qt.NewQIcon()
icon5.AddFile4(":/rsrc/table_delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionDelete_bucket.SetIcon(icon5)
ui.actionRefresh_buckets = qt.NewQAction()
actionRefresh_buckets__objectName := qt.NewQAnyStringView3("actionRefresh_buckets")
ui.actionRefresh_buckets.SetObjectName(*actionRefresh_buckets__objectName)
actionRefresh_buckets__objectName.Delete() // setter copied value
icon6 := qt.NewQIcon()
icon6.AddFile4(":/rsrc/arrow_refresh.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionRefresh_buckets.SetIcon(icon6)
ui.actionClear_selection = qt.NewQAction()
actionClear_selection__objectName := qt.NewQAnyStringView3("actionClear_selection")
ui.actionClear_selection.SetObjectName(*actionClear_selection__objectName)
actionClear_selection__objectName.Delete() // setter copied value
ui.actionNew_database = qt.NewQAction()
actionNew_database__objectName := qt.NewQAnyStringView3("actionNew_database")
ui.actionNew_database.SetObjectName(*actionNew_database__objectName)
actionNew_database__objectName.Delete() // setter copied value
icon7 := qt.NewQIcon()
icon7.AddFile4(":/rsrc/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionNew_database.SetIcon(icon7)
ui.actionAdd_bucket = qt.NewQAction()
actionAdd_bucket__objectName := qt.NewQAnyStringView3("actionAdd_bucket")
ui.actionAdd_bucket.SetObjectName(*actionAdd_bucket__objectName)
actionAdd_bucket__objectName.Delete() // setter copied value
icon8 := qt.NewQIcon()
icon8.AddFile4(":/rsrc/table_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionAdd_bucket.SetIcon(icon8)
ui.actionOpen_database_as_read_only = qt.NewQAction()
actionOpen_database_as_read_only__objectName := qt.NewQAnyStringView3("actionOpen_database_as_read_only")
ui.actionOpen_database_as_read_only.SetObjectName(*actionOpen_database_as_read_only__objectName)
actionOpen_database_as_read_only__objectName.Delete() // setter copied value
ui.actionExport_database_as_zip = qt.NewQAction()
actionExport_database_as_zip__objectName := qt.NewQAnyStringView3("actionExport_database_as_zip")
ui.actionExport_database_as_zip.SetObjectName(*actionExport_database_as_zip__objectName)
actionExport_database_as_zip__objectName.Delete() // setter copied value
ui.actionCreate_database_from_zip = qt.NewQAction()
actionCreate_database_from_zip__objectName := qt.NewQAnyStringView3("actionCreate_database_from_zip")
ui.actionCreate_database_from_zip.SetObjectName(*actionCreate_database_from_zip__objectName)
actionCreate_database_from_zip__objectName.Delete() // setter copied value
ui.centralWidget = qt.NewQWidget(ui.MainWindow.QWidget)
centralWidget__objectName := qt.NewQAnyStringView3("centralWidget")
ui.centralWidget.SetObjectName(*centralWidget__objectName)
centralWidget__objectName.Delete() // setter copied value
ui.gridLayout = qt.NewQGridLayout(ui.centralWidget)
gridLayout__objectName := qt.NewQAnyStringView3("gridLayout")
ui.gridLayout.SetObjectName(*gridLayout__objectName)
gridLayout__objectName.Delete() // setter copied value
ui.gridLayout.SetContentsMargins(0, 0, 0, 0)
ui.gridLayout.SetSpacing(6)
ui.splitter = qt.NewQSplitter(ui.centralWidget)
splitter__objectName := qt.NewQAnyStringView3("splitter")
ui.splitter.SetObjectName(*splitter__objectName)
splitter__objectName.Delete() // setter copied value
ui.splitter.SetOrientation(qt.Horizontal)
ui.splitter.SetChildrenCollapsible(false)
ui.bucketTree = qt.NewQTreeWidget(ui.splitter.QWidget)
bucketTree__objectName := qt.NewQAnyStringView3("bucketTree")
ui.bucketTree.SetObjectName(*bucketTree__objectName)
bucketTree__objectName.Delete() // setter copied value
ui.bucketTree.SetContextMenuPolicy(qt.CustomContextMenu)
ui.bucketTree.SetUniformRowHeights(true)
ui.splitter.AddWidget(ui.bucketTree.QWidget)
ui.stackedWidget = qt.NewQStackedWidget(ui.splitter.QWidget)
stackedWidget__objectName := qt.NewQAnyStringView3("stackedWidget")
ui.stackedWidget.SetObjectName(*stackedWidget__objectName)
stackedWidget__objectName.Delete() // setter copied value
ui.databasePage = qt.NewQWidget(ui.stackedWidget.QWidget)
databasePage__objectName := qt.NewQAnyStringView3("databasePage")
ui.databasePage.SetObjectName(*databasePage__objectName)
databasePage__objectName.Delete() // setter copied value
ui.gridLayout_4 = qt.NewQGridLayout(ui.databasePage)
gridLayout_4__objectName := qt.NewQAnyStringView3("gridLayout_4")
ui.gridLayout_4.SetObjectName(*gridLayout_4__objectName)
gridLayout_4__objectName.Delete() // setter copied value
ui.gridLayout_4.SetContentsMargins(0, 0, 0, 0)
ui.gridLayout_4.SetSpacing(6)
ui.databaseTabWidget = qt.NewQTabWidget(ui.databasePage)
databaseTabWidget__objectName := qt.NewQAnyStringView3("databaseTabWidget")
ui.databaseTabWidget.SetObjectName(*databaseTabWidget__objectName)
databaseTabWidget__objectName.Delete() // setter copied value
ui.databasePropertiesTab = qt.NewQWidget(ui.databaseTabWidget.QWidget)
databasePropertiesTab__objectName := qt.NewQAnyStringView3("databasePropertiesTab")
ui.databasePropertiesTab.SetObjectName(*databasePropertiesTab__objectName)
databasePropertiesTab__objectName.Delete() // setter copied value
ui.gridLayout_2 = qt.NewQGridLayout(ui.databasePropertiesTab)
gridLayout_2__objectName := qt.NewQAnyStringView3("gridLayout_2")
ui.gridLayout_2.SetObjectName(*gridLayout_2__objectName)
gridLayout_2__objectName.Delete() // setter copied value
ui.gridLayout_2.SetContentsMargins(3, 3, 3, 3)
ui.gridLayout_2.SetSpacing(6)
ui.databasePropertiesArea = qt.NewQPlainTextEdit(ui.databasePropertiesTab)
databasePropertiesArea__objectName := qt.NewQAnyStringView3("databasePropertiesArea")
ui.databasePropertiesArea.SetObjectName(*databasePropertiesArea__objectName)
databasePropertiesArea__objectName.Delete() // setter copied value
ui.databasePropertiesArea.SetFrameShape(qt.QFrame__NoFrame)
ui.databasePropertiesArea.SetReadOnly(true)
ui.gridLayout_2.AddWidget2(ui.databasePropertiesArea.QWidget, 0, 0)
icon9 := qt.NewQIcon()
icon9.AddFile4(":/rsrc/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.databaseTabWidget.AddTab2(ui.databasePropertiesTab, icon9, "")
ui.gridLayout_4.AddWidget2(ui.databaseTabWidget.QWidget, 0, 0)
ui.stackedWidget.AddWidget(ui.databasePage)
ui.bucketPage = qt.NewQWidget(ui.stackedWidget.QWidget)
bucketPage__objectName := qt.NewQAnyStringView3("bucketPage")
ui.bucketPage.SetObjectName(*bucketPage__objectName)
bucketPage__objectName.Delete() // setter copied value
ui.gridLayout_3 = qt.NewQGridLayout(ui.bucketPage)
gridLayout_3__objectName := qt.NewQAnyStringView3("gridLayout_3")
ui.gridLayout_3.SetObjectName(*gridLayout_3__objectName)
gridLayout_3__objectName.Delete() // setter copied value
ui.gridLayout_3.SetContentsMargins(0, 0, 0, 0)
ui.gridLayout_3.SetSpacing(6)
ui.bucketTabWidget = qt.NewQTabWidget(ui.bucketPage)
bucketTabWidget__objectName := qt.NewQAnyStringView3("bucketTabWidget")
ui.bucketTabWidget.SetObjectName(*bucketTabWidget__objectName)
bucketTabWidget__objectName.Delete() // setter copied value
ui.bucketPropertiesTab = qt.NewQWidget(ui.bucketTabWidget.QWidget)
bucketPropertiesTab__objectName := qt.NewQAnyStringView3("bucketPropertiesTab")
ui.bucketPropertiesTab.SetObjectName(*bucketPropertiesTab__objectName)
bucketPropertiesTab__objectName.Delete() // setter copied value
ui.gridLayout_5 = qt.NewQGridLayout(ui.bucketPropertiesTab)
gridLayout_5__objectName := qt.NewQAnyStringView3("gridLayout_5")
ui.gridLayout_5.SetObjectName(*gridLayout_5__objectName)
gridLayout_5__objectName.Delete() // setter copied value
ui.gridLayout_5.SetContentsMargins(3, 3, 3, 3)
ui.gridLayout_5.SetSpacing(6)
ui.bucketPropertiesArea = qt.NewQPlainTextEdit(ui.bucketPropertiesTab)
bucketPropertiesArea__objectName := qt.NewQAnyStringView3("bucketPropertiesArea")
ui.bucketPropertiesArea.SetObjectName(*bucketPropertiesArea__objectName)
bucketPropertiesArea__objectName.Delete() // setter copied value
ui.bucketPropertiesArea.SetFrameShape(qt.QFrame__NoFrame)
ui.bucketPropertiesArea.SetReadOnly(true)
ui.gridLayout_5.AddWidget2(ui.bucketPropertiesArea.QWidget, 0, 0)
icon10 := qt.NewQIcon()
icon10.AddFile4(":/rsrc/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.bucketTabWidget.AddTab2(ui.bucketPropertiesTab, icon10, "")
ui.bucketDataTab = qt.NewQWidget(ui.bucketTabWidget.QWidget)
bucketDataTab__objectName := qt.NewQAnyStringView3("bucketDataTab")
ui.bucketDataTab.SetObjectName(*bucketDataTab__objectName)
bucketDataTab__objectName.Delete() // setter copied value
ui.gridLayout_6 = qt.NewQGridLayout(ui.bucketDataTab)
gridLayout_6__objectName := qt.NewQAnyStringView3("gridLayout_6")
ui.gridLayout_6.SetObjectName(*gridLayout_6__objectName)
gridLayout_6__objectName.Delete() // setter copied value
ui.gridLayout_6.SetContentsMargins(3, 3, 3, 3)
ui.gridLayout_6.SetSpacing(6)
ui.bucketData = qt.NewQTreeWidget(ui.bucketDataTab)
bucketData__objectName := qt.NewQAnyStringView3("bucketData")
ui.bucketData.SetObjectName(*bucketData__objectName)
bucketData__objectName.Delete() // setter copied value
ui.bucketData.SetSelectionMode(qt.QAbstractItemView__ExtendedSelection)
ui.bucketData.SetIndentation(0)
ui.bucketData.SetRootIsDecorated(false)
ui.bucketData.SetUniformRowHeights(true)
ui.bucketData.SetItemsExpandable(false)
ui.gridLayout_6.AddWidget3(ui.bucketData.QWidget, 0, 0, 1, 3)
ui.AddDataButton = qt.NewQPushButton(ui.bucketDataTab)
AddDataButton__objectName := qt.NewQAnyStringView3("AddDataButton")
ui.AddDataButton.SetObjectName(*AddDataButton__objectName)
AddDataButton__objectName.Delete() // setter copied value
icon11 := qt.NewQIcon()
icon11.AddFile4(":/rsrc/add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.AddDataButton.SetIcon(icon11)
ui.gridLayout_6.AddWidget2(ui.AddDataButton.QWidget, 1, 0)
/* miqt-uic: no handler for spacer */
ui.DeleteDataButton = qt.NewQPushButton(ui.bucketDataTab)
DeleteDataButton__objectName := qt.NewQAnyStringView3("DeleteDataButton")
ui.DeleteDataButton.SetObjectName(*DeleteDataButton__objectName)
DeleteDataButton__objectName.Delete() // setter copied value
icon12 := qt.NewQIcon()
icon12.AddFile4(":/rsrc/delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.DeleteDataButton.SetIcon(icon12)
ui.gridLayout_6.AddWidget2(ui.DeleteDataButton.QWidget, 1, 1)
icon13 := qt.NewQIcon()
icon13.AddFile4(":/rsrc/table.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.bucketTabWidget.AddTab2(ui.bucketDataTab, icon13, "")
ui.gridLayout_3.AddWidget2(ui.bucketTabWidget.QWidget, 0, 0)
ui.stackedWidget.AddWidget(ui.bucketPage)
ui.splitter.AddWidget(ui.stackedWidget.QWidget)
ui.gridLayout.AddWidget2(ui.splitter.QWidget, 0, 0)
ui.MainWindow.SetCentralWidget(ui.centralWidget) // Set central widget
ui.menuBar = qt.NewQMenuBar(ui.MainWindow.QWidget)
menuBar__objectName := qt.NewQAnyStringView3("menuBar")
ui.menuBar.SetObjectName(*menuBar__objectName)
menuBar__objectName.Delete() // setter copied value
ui.menuBar.Resize(668, 22)
ui.menuFile = qt.NewQMenu(ui.menuBar.QWidget)
menuFile__objectName := qt.NewQAnyStringView3("menuFile")
ui.menuFile.SetObjectName(*menuFile__objectName)
menuFile__objectName.Delete() // setter copied value
ui.menuConvert = qt.NewQMenu(ui.menuFile.QWidget)
menuConvert__objectName := qt.NewQAnyStringView3("menuConvert")
ui.menuConvert.SetObjectName(*menuConvert__objectName)
menuConvert__objectName.Delete() // setter copied value
ui.menuConvert.QWidget.AddAction(ui.actionExport_database_as_zip)
ui.menuConvert.QWidget.AddAction(ui.actionCreate_database_from_zip)
ui.menuFile.QWidget.AddAction(ui.actionNew_database)
ui.menuFile.QWidget.AddAction(ui.actionOpen_database)
ui.menuFile.QWidget.AddAction(ui.actionOpen_database_as_read_only)
ui.menuFile.AddSeparator()
ui.menuFile.AddMenu(ui.menuConvert)
ui.menuFile.AddSeparator()
ui.menuFile.AddSeparator()
ui.menuFile.QWidget.AddAction(ui.actionExit)
ui.menuHelp = qt.NewQMenu(ui.menuBar.QWidget)
menuHelp__objectName := qt.NewQAnyStringView3("menuHelp")
ui.menuHelp.SetObjectName(*menuHelp__objectName)
menuHelp__objectName.Delete() // setter copied value
ui.menuHelp.QWidget.AddAction(ui.actionAbout_qbolt)
ui.menuHelp.QWidget.AddAction(ui.actionAbout_Qt)
ui.menuView = qt.NewQMenu(ui.menuBar.QWidget)
menuView__objectName := qt.NewQAnyStringView3("menuView")
ui.menuView.SetObjectName(*menuView__objectName)
menuView__objectName.Delete() // setter copied value
ui.menuView.QWidget.AddAction(ui.actionClear_selection)
ui.menuBar.AddMenu(ui.menuFile)
ui.menuBar.AddMenu(ui.menuView)
ui.menuBar.AddMenu(ui.menuHelp)
ui.MainWindow.SetMenuBar(ui.menuBar)
ui.mainToolBar = qt.NewQToolBar(ui.MainWindow.QWidget)
mainToolBar__objectName := qt.NewQAnyStringView3("mainToolBar")
ui.mainToolBar.SetObjectName(*mainToolBar__objectName)
mainToolBar__objectName.Delete() // setter copied value
ui.MainWindow.AddToolBar(qt.TopToolBarArea, ui.mainToolBar)
/* miqt-uic: no handler for mainToolBar attribute 'toolBarBreak' */
ui.mainToolBar.QWidget.AddAction(ui.actionNew_database)
ui.mainToolBar.QWidget.AddAction(ui.actionOpen_database)
ui.mainToolBar.AddSeparator()
ui.statusBar = qt.NewQStatusBar(ui.MainWindow.QWidget)
statusBar__objectName := qt.NewQAnyStringView3("statusBar")
ui.statusBar.SetObjectName(*statusBar__objectName)
statusBar__objectName.Delete() // setter copied value
ui.MainWindow.SetStatusBar(ui.statusBar)
ui.Retranslate()
ui.stackedWidget.SetCurrentIndex(0)
ui.databaseTabWidget.SetCurrentIndex(0)
ui.bucketTabWidget.SetCurrentIndex(0)
return ui
}
// Retranslate reapplies all text translations.
func (ui *MainWindowUi) Retranslate() {
ui.actionAbout_qbolt.SetText(qt.QMainWindow_Tr("&About QBolt"))
ui.actionAbout_qbolt.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F1")))
ui.actionAbout_Qt.SetText(qt.QMainWindow_Tr("About &Qt"))
ui.actionOpen_database.SetText(qt.QMainWindow_Tr("&Open database..."))
ui.actionOpen_database.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("Ctrl+O")))
ui.actionExit.SetText(qt.QMainWindow_Tr("&Exit"))
ui.actionDisconnect.SetText(qt.QMainWindow_Tr("Disconnect"))
ui.actionDelete_bucket.SetText(qt.QMainWindow_Tr("Delete bucket"))
ui.actionRefresh_buckets.SetText(qt.QMainWindow_Tr("Refresh buckets"))
ui.actionRefresh_buckets.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F5")))
ui.actionClear_selection.SetText(qt.QMainWindow_Tr("&Clear selection"))
ui.actionNew_database.SetText(qt.QMainWindow_Tr("&New database..."))
ui.actionAdd_bucket.SetText(qt.QMainWindow_Tr("Add bucket..."))
ui.actionOpen_database_as_read_only.SetText(qt.QMainWindow_Tr("Open database as read-only..."))
ui.actionExport_database_as_zip.SetText(qt.QMainWindow_Tr("Export database as zip"))
ui.actionCreate_database_from_zip.SetText(qt.QMainWindow_Tr("Create database from zip"))
ui.bucketTree.HeaderItem().SetText(0, qt.QTreeWidget_Tr("Bucket"))
ui.databaseTabWidget.SetTabText(ui.databaseTabWidget.IndexOf(ui.databasePropertiesTab), qt.QTabWidget_Tr("Database"))
ui.databasePropertiesArea.SetPlainText(qt.QWidget_Tr("No selection"))
ui.bucketTabWidget.SetTabText(ui.bucketTabWidget.IndexOf(ui.bucketPropertiesTab), qt.QTabWidget_Tr("Bucket"))
ui.bucketTabWidget.SetTabText(ui.bucketTabWidget.IndexOf(ui.bucketDataTab), qt.QTabWidget_Tr("Data"))
ui.bucketData.HeaderItem().SetText(0, qt.QTreeWidget_Tr("Key"))
ui.bucketData.HeaderItem().SetText(1, qt.QTreeWidget_Tr("Data length"))
ui.AddDataButton.SetText(qt.QWidget_Tr("Add..."))
ui.DeleteDataButton.SetText(qt.QWidget_Tr("Delete..."))
ui.menuFile.SetTitle(qt.QMenuBar_Tr("&File"))
ui.menuConvert.SetTitle(qt.QMenu_Tr("Convert"))
ui.menuHelp.SetTitle(qt.QMenuBar_Tr("Help"))
ui.menuView.SetTitle(qt.QMenuBar_Tr("&View"))
}

17
resources.go Normal file
View File

@@ -0,0 +1,17 @@
package main
//go:generate miqt-rcc -Input "resources.qrc" -OutputGo "resources.go" -OutputRcc "resources.rcc" -Qt6
import (
"embed"
qt "github.com/mappu/miqt/qt6"
)
//go:embed resources.rcc
var _resourceRcc []byte
func init() {
_ = embed.FS{}
qt.QResource_RegisterResourceWithRccData(&_resourceRcc[0])
}

17
resources.qrc Normal file
View File

@@ -0,0 +1,17 @@
<RCC>
<qresource prefix="/">
<file>rsrc/database_add.png</file>
<file>rsrc/table.png</file>
<file>rsrc/information.png</file>
<file>rsrc/database_lightning.png</file>
<file>rsrc/database.png</file>
<file>rsrc/table_add.png</file>
<file>rsrc/table_delete.png</file>
<file>rsrc/add.png</file>
<file>rsrc/delete.png</file>
<file>rsrc/chart_bar.png</file>
<file>rsrc/arrow_refresh.png</file>
<file>rsrc/disconnect.png</file>
<file>rsrc/door_out.png</file>
</qresource>
</RCC>

BIN
resources.rcc Normal file

Binary file not shown.

BIN
rsrc/add.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

BIN
rsrc/arrow_refresh.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

BIN
rsrc/chart_bar.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

BIN
rsrc/database.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

BIN
rsrc/database_add.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

BIN
rsrc/database_connect.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

BIN
rsrc/database_lightning.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

BIN
rsrc/delete.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

BIN
rsrc/disconnect.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

BIN
rsrc/door_out.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 B

BIN
rsrc/information.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

BIN
rsrc/page.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

BIN
rsrc/qbolt.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
rsrc/table.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

BIN
rsrc/table_add.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

BIN
rsrc/table_delete.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

40
util.go Normal file
View File

@@ -0,0 +1,40 @@
package main
import (
"strconv"
)
func getDisplayName(qba []byte) string {
if len(qba) ==0 {
return "<empty>"
}
ret := strconv.Quote(string(qba))
return ret[1 : len(ret)-1]
}
// ReverseSlice reverses a slice.
// @ref https://stackoverflow.com/a/28058324
func ReverseSlice[S ~[]E, E any](s S) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
// CopySliceAdd copies a slice and adds one more thing on the end.
func CopySliceAdd[T comparable](base []T, and T) []T {
ret := make([]T, len(base)+1)
copy(ret, base)
ret[len(ret)-1] = and
return ret
}
// Apply creates a new slice where every element of the input arr is transformed.
func Apply[T comparable](arr []T, transform func(T) T) []T {
ret := make([]T, len(arr))
for i := 0; i < len(arr); i++ {
ret[i] = transform(arr[i])
}
return ret
}

17
util_test.go Normal file
View File

@@ -0,0 +1,17 @@
package main
import (
"testing"
)
func TestQByteArray(t *testing.T) {
foo := "the \x00 quick \x01 brown \x02 fox \x03 jumps \x04 over \x05 the \x06 lazy \x07 dog"
qb := MakeQByteArray(foo)
out := FromQByteArray(qb)
if foo != out {
t.Errorf("Expected equal")
}
}

View File

@@ -0,0 +1,50 @@
{
"RT_GROUP_ICON": {
"APP": {
"0000": "rsrc/database_lightning.png"
}
},
"RT_VERSION": {
"#1": {
"0000": {
"fixed": {
"file_version": "%VERSION%.0",
"product_version": "%VERSION%.0"
},
"info": {
"0409": {
"FileDescription": "QBolt Database Viewer",
"FileVersion": "%VERSION%.0",
"ProductName": "QBolt Database Viewer",
"ProductVersion": "%VERSION%.0"
}
}
}
}
},
"RT_MANIFEST": {
"#1": {
"0409": {
"identity": {
"name": "",
"version": ""
},
"description": "",
"minimum-os": "win7",
"execution-level": "as invoker",
"ui-access": false,
"auto-elevate": false,
"dpi-awareness": "per monitor v2",
"disable-theming": false,
"disable-window-filtering": false,
"high-resolution-scrolling-aware": false,
"ultra-high-resolution-scrolling-aware": false,
"long-path-aware": false,
"printer-driver-isolation": false,
"gdi-scaling": false,
"segment-heap": false,
"use-common-controls-v6": true
}
}
}
}