241 Commits

Author SHA1 Message Date
13fedfa2f6 doc: update README and TODO 2025-12-22 19:48:36 +13:00
8b06ccef48 buntdb: add support, including editing+shrink 2025-12-22 18:43:48 +13:00
17d6b5172d ui: use compress icon for bolt-zip tool 2025-12-22 18:23:13 +13:00
708e8072ff sqlite: support ssh connections 2025-12-22 18:17:41 +13:00
c8125d2c84 sqliteclidriver: add ssh backend 2025-12-22 18:17:35 +13:00
a5138a51f3 sqliteclidriver: refactor cmd handling to support other implementations 2025-12-22 18:17:29 +13:00
02b5a8fd48 sqliteclidriver: add more comments, expose DriverName const 2025-12-22 18:17:06 +13:00
b2118c9196 config: add omitempty for many db types 2025-12-22 18:16:35 +13:00
5aa76a465c sshtunnel: add icons for connection modes, add omitempty 2025-12-22 18:16:11 +13:00
f3e729b023 assets: add compress, key, page_key icons 2025-12-22 18:15:55 +13:00
5c7a99d16b makefile: autogenerate embed.qrc from asset files 2025-12-22 18:15:49 +13:00
4d5ca19b47 doc/changelog: changelog for v2.0.0 2025-12-20 19:19:46 +13:00
a0e70636a1 doc/LICENSE: merge qbolt/yvbolt license headers 2025-12-20 19:19:46 +13:00
13e139e023 app: show version in main window titlebar 2025-12-20 19:19:46 +13:00
1f7d03e67a doc: update screenshots 2025-12-20 19:19:46 +13:00
0b1f662e99 doc/README: add other features from qbolt's readme 2025-12-20 19:19:46 +13:00
e5f36b0f66 doc/README: remove yvbolt/qbolt cross reference link 2025-12-20 19:19:46 +13:00
0eccb12744 doc/README: add direct links to changelog/downloads 2025-12-20 19:19:46 +13:00
f2d3240153 git: merge yvbolt/qbolt changelogs as separate file 2025-12-20 19:19:46 +13:00
9fb4302000 git: merge all yvbolt/qbolt names 2025-12-20 19:19:40 +13:00
e3f94f1eba git: synthetic merge of qbolt and yvbolt branches 2025-12-20 18:32:58 +13:00
2259b3f455 doc: update changelog for v0.11.0 2025-12-20 18:24:24 +13:00
b4e8733798 doc: update TODO 2025-12-20 18:24:17 +13:00
ace5e3e65e bolt: support import/export as zip 2025-12-20 18:10:55 +13:00
e132500fd8 util: add slice_and, slice_apply 2025-12-20 18:10:33 +13:00
a6bb412a34 bolt: add comments on boltTargetBucket 2025-12-20 18:10:24 +13:00
51f2a69ed2 badger, sqlite: fix *.* spec for 'all files' in export file path 2025-12-20 17:46:07 +13:00
1db2d9781d ssh-agent: support lock/unlock 2025-12-20 17:21:35 +13:00
9f662a7fa2 badger: move encryption options to sub-struct [BREAKING] 2025-12-20 17:21:28 +13:00
034bd8114a ssh-agent: add initial support 2025-12-20 17:02:23 +13:00
7242e8644b ssh-tunnel: support ssh-agents 2025-12-20 16:43:07 +13:00
820285066b doc: update TODO 2025-12-19 19:17:46 +13:00
94c517a324 secretsvc: treat data column as binary 2025-12-19 19:11:11 +13:00
9fbc7b4ee6 config: fix saving new entries causing order to be lost 2025-12-19 19:06:51 +13:00
421cabb7a5 lmdb: support truncating existing childdb 2025-12-19 19:05:39 +13:00
910ef0dd9a makefile: add helper for running test mongodb server 2025-12-19 18:49:01 +13:00
a9c6b135c7 mongo: show index details in properties pane 2025-12-19 18:48:52 +13:00
02d4b918d6 ui: scroll to top when changing tables 2025-12-19 18:43:02 +13:00
0798547b39 ssh known_hosts support 2025-12-19 18:23:07 +13:00
3d114ec1a4 app: prevent editing if table did not load data 2025-12-19 17:55:41 +13:00
4913e36ad6 doc: changelog for v0.10.1 2025-12-17 18:17:51 +13:00
bafabdf690 makefile: build release binaries with go1.26rc1 2025-12-17 17:28:06 +13:00
4f2381ee33 go mod tidy 2025-12-17 17:27:58 +13:00
d876013ade main: fix mixing qt5 + qt6 libraries(!) 2025-12-17 17:27:54 +13:00
e42eb6ace7 doc: changelog for v0.10.0 2025-12-16 20:32:10 +13:00
4789c8c706 doc: update README and TODO 2025-12-16 20:31:06 +13:00
cad3e9d496 table: refactor typed-column handling 2025-12-16 19:55:25 +13:00
fc63b992ca driver-versions: fix missing space 2025-12-16 19:55:11 +13:00
1877417327 mongo: replace fake-edit bson with real readonly bson support 2025-12-16 17:29:31 +13:00
13b5878fe5 ui: 'driver versions' show full detail in new table 2025-12-16 17:08:08 +13:00
fe5d218291 pebble: remove driver-version display in status bar 2025-12-16 17:07:51 +13:00
2abca95d72 hexview: tighter column layout 2025-12-16 16:57:31 +13:00
7228bc5ba7 table: for []byte columns, dynamically switch between textedit + hexview 2025-12-16 16:57:23 +13:00
3087ba498d table: use hexview widget for []byte-type data 2025-12-15 19:18:36 +13:00
847521541f hexview: initial commit 2025-12-15 19:18:24 +13:00
1379e912e5 app: move monospace() to helper function 2025-12-15 19:18:06 +13:00
69e17868a4 mongodb: show BSON editor in an autoconfig popup 2025-12-14 12:11:17 +13:00
45650ead8c mongo: add bson_find_id helper function 2025-12-14 11:28:11 +13:00
e1f8c40143 bolt, lmdb, mongo: add confirmation warning for destructive actions 2025-12-14 11:27:38 +13:00
f3496db2c7 doc: update README and TODO 2025-12-12 18:57:52 +13:00
13511a389a rosedb: initial support, including editing 2025-12-12 18:54:52 +13:00
2c1faf98c7 bitcask: initial support, including editing + backup 2025-12-12 18:42:18 +13:00
9a42a4021d lotusdb: optimize png logo 2025-12-12 18:42:00 +13:00
d2587949d6 lotusdb: initial support, including editing 2025-12-12 18:18:52 +13:00
7c24b1f24a doc: update README and TODO 2025-12-11 20:02:35 +13:00
882517dffc mongodb: support querying 2025-12-11 19:52:33 +13:00
f98db57a97 queryable: pass in bucketPath 2025-12-11 19:52:25 +13:00
7a68362149 ui: fix pasting causing rich text in query editor 2025-12-11 19:52:09 +13:00
f716afee74 mongodb: ssh tunnel support 2025-12-11 19:13:55 +13:00
aecab00e70 mongodb: add/remove databases and collections 2025-12-11 18:55:28 +13:00
65d921ddde mongodb: initial support 2025-12-11 18:48:09 +13:00
5d39d90044 redis: minor bump in driver version 2025-12-11 18:08:26 +13:00
6a685a2562 ui: trigger 'unsaved changes' warning when changing nav 2025-12-10 19:50:58 +13:00
d4ee91ed10 doc: update README and TODO 2025-12-10 19:33:32 +13:00
9fc11d8c61 sqlite: fix primary key handling when editing 2025-12-10 19:06:56 +13:00
cb09e0eb84 Merge branch 'tableview'
# Conflicts:
#	db_badger.go
2025-12-10 18:56:31 +13:00
12a60f57e3 doc/TODO: update 2025-12-10 18:54:43 +13:00
a3e32178ac starskey: fix k/v handling, support editing 2025-12-10 18:54:09 +13:00
9e76cb32fe pebble: support editing 2025-12-10 18:38:12 +13:00
63e47ae505 leveldb: support editing 2025-12-10 18:29:26 +13:00
319da7c13c badger: support editing 2025-12-10 18:26:00 +13:00
e03c635e7b bolt, lmdb: use common code for applying changes to k/v store 2025-12-10 18:21:53 +13:00
b2b5f8ba54 sqlite: new editing support 2025-12-10 18:19:20 +13:00
dcff85cfe5 lmdb: new editing support 2025-12-10 18:04:58 +13:00
65cbfec7af tableview: editing support 2025-12-10 18:00:23 +13:00
c2c997e53f tableview: fix crashes 2025-12-09 16:53:42 +13:00
9d2afaf57c connection-manager: auto save with preferred displayName 2025-12-09 16:53:30 +13:00
8b410bca89 badger: fix wrong value column 2025-12-09 16:39:23 +13:00
e11b5b2100 ui: port to qtableview 2025-12-09 16:38:50 +13:00
f292780972 doc/TODO: log bug in connection manager 2025-12-09 16:38:17 +13:00
89bd3ed27a makefile: embed version in binary
Avoiding using a git tag for this because miqt-docker already sets
the ldflags argument
2025-12-03 20:03:16 +13:00
2a22e92be4 makefile: add Windows icon and properties 2025-12-03 20:03:16 +13:00
5d829befca makefile: SOURCES needs 'shell' 2025-12-03 20:03:16 +13:00
2eb7385516 makefile: use conditional variables for helper tools 2025-12-03 20:03:16 +13:00
543f573c7f autoconfig: move defaults to Reset(), choose Bolt by default 2025-12-03 19:28:33 +13:00
463daba2cf autoconfig: fix labels for new autoconfig camelcase detection 2025-12-03 19:28:33 +13:00
9588e5189e autoconfig: update v0.4.1 for windows flicker issue 2025-12-03 19:28:25 +13:00
30716df112 doc/README: changelog for v0.9.0 2025-12-02 19:08:08 +13:00
ad8af93545 makefile: the linux release artefact should work on deb12 2025-12-02 19:00:39 +13:00
c306a6d1a5 doc: update README and TODO files 2025-12-02 17:09:33 +13:00
6a90605bd1 secret-service: initial commit 2025-12-02 17:01:49 +13:00
c69089841a ui: fix recursive triggering of on-edit signal, reinstate explicit-edit colours 2025-12-02 13:58:34 +13:00
ddbc30ed01 connection-manager: support import/export of saved connections 2025-12-01 23:17:56 +13:00
292e13a3e0 ui: fix panic in setting cell background colour (not fully understood) 2025-12-01 23:06:31 +13:00
7aa6703ee0 app: fix row-deletion from triggering the edit callback 2025-12-01 23:06:01 +13:00
f90f76c097 doc: update README and TODO files 2025-12-01 19:39:00 +13:00
134d4cf290 lmdb: edit support (minimal) 2025-12-01 19:38:01 +13:00
773470b30c lmdb: size key buffer based on database's maximum key size 2025-12-01 19:25:11 +13:00
f55ab455be ui: adjust recursive thresholds for preloading child nav 2025-12-01 18:56:15 +13:00
b77ea21378 connection-manager: load using saved displayname 2025-12-01 18:56:04 +13:00
002298b4ff lmdb: add multi-db mode, add create/delete child databases 2025-12-01 18:55:55 +13:00
293627ab17 lmdb: initial commit 2025-12-01 18:20:20 +13:00
14219d6d49 doc/TODO: update status 2025-11-30 19:34:43 +13:00
37a6f479b8 starskey: initial read-only implementation 2025-11-30 19:34:09 +13:00
176c51549c app: add defensive panics/error handling 2025-11-30 18:33:06 +13:00
17c0fb3332 app: use gob instead of json for stashing bucketPaths 2025-11-30 18:32:51 +13:00
4a4e8a76a3 config: fix test cleanliness in error wrappers 2025-11-30 18:31:59 +13:00
af77b83e27 doc: update README and TODO files 2025-11-30 17:53:25 +13:00
6b187c4142 debconf: group applications by first slash 2025-11-30 17:46:27 +13:00
da56321624 assets: re-optimize all embed images, strip colour-profile data 2025-11-30 17:13:37 +13:00
4447e149b9 bolt: add default names for autosaved connections 2025-11-30 17:04:38 +13:00
5850ba8836 connection-manager: refactor move config save/load to separate file 2025-11-30 17:04:31 +13:00
81206ae1f0 ssh: do not save H1 header field in json blob 2025-11-30 17:01:48 +13:00
5be200c672 ui: add comment re hidpi scaling 2025-11-30 17:01:39 +13:00
25d8af1043 ui: accurate transparency for properties text area 2025-11-30 17:01:34 +13:00
6abb5b6159 connection-manager: full implementation 2025-11-30 17:01:20 +13:00
1a20ca1a9c connection-manager: prompt to save on successful connection 2025-11-29 16:37:35 +13:00
38c5b88055 ssh: replace auth fields with OneOf 2025-11-29 16:11:35 +13:00
fb20955bf1 config: replace DB type registration with autoconfig.OneOf 2025-11-29 16:11:26 +13:00
8f76b5858d autoconfig: upgrade to v0.3.0 2025-11-29 16:11:10 +13:00
d8cad1e59e connection-manager: WIP 2025-11-25 18:18:12 +13:00
6bb1d0ab9f ui: add 'about qt' handling 2025-11-25 17:27:43 +13:00
35e06396e1 gui: redesign layout to use global toolbar, toggle editable 2025-11-25 17:27:36 +13:00
649cff7178 filter: fix all files *.* to use plain * 2025-11-25 17:27:08 +13:00
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
6fb8d1ba0c commit all archived files 2017-06-19 00:00:00 +00: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
8d8374be24 initial meta commit 2017-05-21 00:00:00 +00: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
103 changed files with 5636 additions and 1473 deletions

11
.gitignore vendored
View File

@@ -1,8 +1,5 @@
testdata/ testdata/
liblcl-*.zip qbolt
liblcl.so qbolt.exe
liblcl.dll qbolt.linux64.tar.xz
yvbolt qbolt.win64.zip
yvbolt.exe
yvbolt.linux64.tar.xz
yvbolt.win64.zip

190
CHANGELOG.md Normal file
View File

@@ -0,0 +1,190 @@
# Changelog
2025-12-20 v2.0.0
- Merge yvbolt and QBolt together
- App: Show application version in main window titlebar
2025-12-20 v0.11.0 (yvbolt)
- Badger: Change encryption configuration options (**BREAKING**)
- SSH Agent: Initial support, including lock/unlock
- Badger, SQLite: Fix "all files" spec when choosing an export file path
- Bolt: Support import/export as zip archive
- Support SSH agents when using SSH tunnels (e.g. for Redis or Mongo)
2025-12-17 v0.10.1 (yvbolt)
- Fix mixed Qt 5 / Qt 6 syntax
- Build release binaries with Go1.26
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.10.1)
2025-12-16 v0.10.0 (yvbolt)
- MongoDB: Initial support, including SSH tunnel, managing collections, traversing BSON documents, and querying
- LotusDB: Initial support, including editing
- Bitcask: Initial support, including editing and backup
- RoseDB: Initial support, including editing
- Badger: Fix k/v column display, support editing
- LMDB: Support editing, add warning for destructive actions
- LevelDB: Support editing
- Pebble: Support editing
- Starskey: Fix k/v column display, support editing
- Redis: Bump driver version v9.16.0 -> v9.17.2
- Bolt: Add warning for destructive actions
- Connection manager: When saving, use the database's preferred name
- App: Add hex viewer for binary data if it is not valid UTF-8
- App: Use virtualized table renderer
- App: Use multiple plug-in typed column stores in table backend
- App: Fix graphical flicker when editing connections on Windows
- App: Set up icon and exe properties on Windows
- App: Embed version number in release builds
- App: Show all driver versions as a virtual data table
- App: Show unsaved-changes warning also when changing database
- App: Fix rich text formatting appearing when pasting into the query window
2025-12-02 v0.9.0 (yvbolt)
- LMDB: Initial support, including multi-database mode, data editing, and managing child databases
- Starskey: Initial support
- Freedesktop.org Secret Service: Initial support, including unlocking collections and creating child collections
- App: Use global toolbar style
- App: Add connection manager, encrypting credentials with AEAD AES256-GCM using OS keychain
- App: Offer to save valid quick-connection to the connection manager
- App: Add 'About Qt' menu option
- App: Fixed file extension filter for database files with no extension
- App: Fixed background colour for Properties area on different OSes
- App: Fixed libpng warnings about greyscale image data for embedded logo images
- App: Fixed issue with non-UTF8 child database names
- Debconf: Improve navigation speed by splitting applications into virtual tables
- SSH: Redesign options to pick only one of the available auth methods
- Fixed running release binary on Debian 12
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.9.0)
2025-11-23 v0.8.0 (yvbolt)
- Port from [liblcl](https://github.com/ying32/liblcl) to [MIQT Qt 6](https://github.com/mappu/miqt)
- Badger: Upgrade v4.2.0 -> v4.8.0
- Pebble: Upgrade v1.0.0 -> v1.1.5
- SQLite: Upgrade v1.14.22 -> v1.14.32
- Redis: Upgrade v9.5.3 -> v9.16.0
- Bolt: Upgrade v1.4.0-alpha.1 -> v1.4.3
- Bolt: Fix child buckets appearing in data area
- Badger, Pebble, Debconf: Remove redundant "Data" navigation layer
- Badger: Support encrypted databases
- Badger: Support readonly databases
- Badger: Add context-menu actions for backup, restore, and compact
- Pebble: Support readonly databases
- LevelDB: Add LevelDB database integration
- Redis: Support SSH tunnel
- SQLite: Allow editing the primary key column
- App: New style connection dialog
- App: Updated keyboard shortcuts (Ctrl+O to open new connection, F5 to refresh, F9 to execute query)
- App: Add confirmation when refreshing the data table if there are uncommitted changes
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.8.0)
2025-05-04 1.1.0 (qbolt)
- 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 (qbolt)
- 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)
2024-07-18 v0.7.0 (yvbolt)
- SQLite, Bolt: Initial support for editing data (insert, per-cell update, delete)
- SQLite: Add context menu actions for compact (vacuum), export, and drop table
- App: New grid widget
- App: Add refresh button
- App: Bigger window size, use icons for toolbars, better UI colours for Windows
- App: Prevent submitting blank queries to database
- Refactor database interface and error handling
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.7.0)
2024-06-30 v0.6.0 (yvbolt)
- Debconf: Add as supported database
- SQLite: Support table names containing special characters
- SQLite: Improvements for experimental command-line driver
- Redis: Improve connection dialog window position
- App: Cosmetic fixes for frame borders, help dialog, and Windows fonts+colours
- Build: Change compression parameters for release builds
- Build: Compile CGO with -O2 for release builds
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.6.0)
2024-06-29 v0.5.0 (yvbolt)
- Pebble: Add as supported database
- Bolt: Support opening as readonly
- Bolt: Support creating new databases
- Bolt: Support adding/removing recursive child buckets
- SQLite: Support custom CLI driver that parses `/usr/bin/sqlite3 -json` output (experimental)
- Redis: Improve query parser to support quoted strings
- App: Support refreshing elements in nav tree
- App: Help menu option to show driver versions
- App: Add image icons for refresh and close context menu actions
- Build: Add makefile for cross-compiling release binaries
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.5.0)
2024-06-23 v0.4.0 (yvbolt)
- Redis: Add as supported database
- Badger: Allow creating in-memory databases
- App: Allow selecting partial query text to execute
- App: Allow closing database connections from context menu
- App: Allow scrolling large content on Properties pane
- App: Preload recursive navigation
- App: Automatically switch to selected database when new connection is created
- App: Add help website link
- App: Add database logo images
2024-06-25 v0.3.0 (yvbolt)
- Badger: Add BadgerDB v4 as supported database
- SQLite: Add support for CGo-free SQLite driver under cross-compilation
- Bolt: Update Bolt to v1.4.0-alpha.1
- App: Add support for running custom queries
- App: Add status bar showing currently selected DB
- App: Fix missing icons in nav when selecting items
- App: Fix extra quotemarks when browsing string content of database
2024-06-08 v0.2.0 (yvbolt)
- SQLite: Add SQLite support (now requires CGo)
- App: Add images for menu and navigation items
2024-06-03 v0.1.0 (yvbolt)
- Initial public release
2020-04-12 1.0.2 (qbolt)
- 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 (qbolt)
- 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
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.1)
2017-05-21 1.0.0 (qbolt)
- 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)

View File

@@ -1,7 +1,8 @@
ISC License ISC License
Copyright 2024 mappy Copyright 2025 mappy
Copyright 2024 The yvbolt Author(s) Copyright 2025 The QBolt Author(s)
Copyright 2025 The yvbolt Author(s)
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

View File

@@ -1,40 +1,68 @@
SHELL:=/bin/bash SHELL:=/bin/bash
SOURCES=$(find . -name '*.go' -type f) SOURCES := $(shell find . -name '*.go' -type f)
GIT_REV := $(shell git describe --exact-match --tags 2>/dev/null || printf "%s-%s" $$(git describe --tags --abbrev=0) $$(git rev-parse HEAD | head -c8))
.DEFAULT_GOAL := dist .DEFAULT_GOAL := dist
MIQT_UIC ?= ~/go/bin/miqt-uic
MIQT_RCC ?= ~/go/bin/miqt-rcc
MIQT_DOCKER ?= ~/go/bin/miqt-docker
GO_WINRES ?= ~/go/bin/go-winres
.PHONY: generate .PHONY: generate
generate: generate:
~/go/bin/miqt-uic -InFile mainwindow.ui -OutFile mainwindow.go -Qt6 /bin/bash -c '( echo "<RCC>" ; echo " <qresource prefix=\"/\">" ; for f in assets/* ; do echo " <file>$$f</file>" ; done ; echo " </qresource>" ; echo "</RCC>" ) > embed.qrc'
~/go/bin/miqt-uic -InFile connectionDialog.ui -OutFile connectionDialog.go -Qt6 $(MIQT_UIC) -InFile mainwindow.ui -OutFile mainwindow.go -Qt6
~/go/bin/miqt-rcc -Input embed.qrc -Qt6 $(MIQT_UIC) -InFile connectionDialog.ui -OutFile connectionDialog.go -Qt6
$(MIQT_UIC) -InFile connectionManagerDialog.ui -OutFile connectionManagerDialog.go -Qt6
$(MIQT_RCC) -Input embed.qrc -Qt6
.PHONY: designer .PHONY: designer
designer: designer:
/usr/lib/qt6/bin/designer & /usr/lib/qt6/bin/designer &
yvbolt: $(SOURCES) .PHONY: optimize-images
~/go/bin/miqt-docker native -minify-build optimize-images:
chmod 755 yvbolt # Strip iCCC colour chunks that libpng/Qt complain about at runtime
upx --lzma yvbolt for f in assets/*.png ; do convert "$$f" -strip "$$f" ; done
optipng -quiet -o5 assets/*.png
make generate
yvbolt.exe: $(SOURCES) qbolt: $(SOURCES)
~/go/bin/miqt-docker win64-cross-go1.24-qt6.5-static -windows-build --tags=windowsqtstatic # Target a debian-12 baseline build
upx --lzma yvbolt.exe sed -i -re 's/".+"/"$(GIT_REV)"/g' version.go
$(MIQT_DOCKER) linux64-go1.26-qt6.4-dynamic -minify-build
git checkout -- version.go
chmod 755 qbolt
upx --lzma qbolt
yvbolt.apk: $(SOURCES) qbolt.exe: $(SOURCES)
~/go/bin/miqt-docker android-armv8a-go1.23-qt6.6-dynamic -android-build sed -i -re 's/".+"/"$(GIT_REV)"/g' version.go
$(MIQT_DOCKER) win64-cross-go1.26-qt6.5-static -windows-build --tags=windowsqtstatic
git checkout -- version.go
$(GO_WINRES) patch --in winres.json --no-backup --product-version git-tag --file-version git-tag qbolt.exe
upx --lzma qbolt.exe
yvbolt.linux64.tar.xz: yvbolt qbolt.apk: $(SOURCES)
rm -f yvbolt.linux64.tar.xz $(MIQT_DOCKER) android-armv8a-go1.23-qt6.6-dynamic -android-build
XZ_OPT='-T0 -9' tar caf yvbolt.linux64.tar.xz --owner=0 --group=0 yvbolt
yvbolt.win64.zip: yvbolt.exe qbolt.linux64.tar.xz: qbolt
rm -f yvbolt.win64.zip rm -f qbolt.linux64.tar.xz
zip -9 yvbolt.win64.zip yvbolt.exe XZ_OPT='-T0 -9' tar caf qbolt.linux64.tar.xz --owner=0 --group=0 qbolt
qbolt.win64.zip: qbolt.exe
rm -f qbolt.win64.zip
zip -9 qbolt.win64.zip qbolt.exe
.PHONY: dist .PHONY: dist
dist: yvbolt.linux64.tar.xz yvbolt.win64.zip dist: qbolt.linux64.tar.xz qbolt.win64.zip
.PHONY: clean .PHONY: clean
clean: clean:
rm -f yvbolt.exe yvbolt yvbolt.linux64.tar.xz yvbolt.win64.zip git checkout -- version.go
rm -f qbolt.exe qbolt qbolt.linux64.tar.xz qbolt.win64.zip
#####
# Test databases in Docker
.PHONY: test-mongo
test-mongo:
sudo docker run --rm -p 127.0.0.1:27017:27017 -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=toor mongo:latest

149
README.md
View File

@@ -1,4 +1,4 @@
# yvbolt # QBolt
A graphical interface for multiple databases. A graphical interface for multiple databases.
@@ -8,131 +8,50 @@ A graphical interface for multiple databases.
- Connect to multiple databases at once - Connect to multiple databases at once
- Browse table/bucket content - Browse table/bucket content
- Use context menu to perform special table/bucket actions - Use context menu to perform special table/bucket actions
- Edit content, and add/delete rows for supported databases
- View database/bucket statistics and metadata
- Run custom SQL queries - Run custom SQL queries
- Select text to run partial query - Select text to run partial query
- Safe handling for non-UTF8 key and data fields - Safe handling for non-UTF8 key and data fields
- Hex viewer for binary data
See also [qbolt](https://code.ivysaur.me/qbolt) for more/different functionality. - Connection Manager saves connections with AEAD AES256-GCM using OS keychain
## Supported databases ## Supported databases
Database |Read |Editing |Connection options |Context menu actions There are currently 16 supported databases:
-------------|------|---------|--------------------|--------
Badger v4 |Yes |No |Encrypted, readonly, in-memory |Backup, restore, compact Database |Read |Editing |Query |Connection options |Context menu actions
Bolt |Yes |Yes |Readonly |Create/delete child buckets -------------|------|---------|------|--------------------|--------
Debconf |Yes |No | | Badger v4 |Yes |Yes |No |Encrypted, readonly, in-memory |Backup, restore, compact
LevelDB |Yes |No |Readonly | Bitcask |Yes |Yes |No |Readonly, autorecovery |Backup
Pebble |Yes |No |Readonly, in-memory | BuntDB |Yes |Yes |No |In-memory |Shrink
Redis |Yes |No |SSH tunnel, RESP v3 | Bolt |Yes |Yes |No |Readonly |Create/delete child buckets, import/export as zip
SQLite |Yes |Yes |CLI driver, in-memory |Vacuum, export Debconf |Yes |No |No | |
Freedesktop.org Secret Service |Yes |No | No | |Unlock, create new collection
LevelDB |Yes |Yes |No |Readonly |
LMDB |Yes |Yes |No |Multi-DB, readonly |Create/delete child databases
LotusDB |Yes |Yes |No | |
MongoDB |Yes |No |Yes |SSH tunnel |Create/delete child databases and collections
Pebble |Yes |Yes |No |Readonly, in-memory |
Redis |Yes |No |Yes |SSH tunnel, RESP v3 |
RoseDB |Yes |Yes |No | |
SQLite |Yes |Yes |Yes |**SSH tunnel**, CLI driver, in-memory |Vacuum, export
SSH Agent |Yes |No |No |Unix/TCP |Lock, unlock
Starskey |Yes |Yes |No |Compression |
## License ## License
The code in this project is licensed under the ISC license (see `LICENSE` file for details). The code in this project is licensed under the ISC license (see `LICENSE` file for details) with the following caveats:
This project redistributes images from the famfamfam/silk icon set under the [CC-BY 2.5 license](http://creativecommons.org/licenses/by/2.5/). - This project depends on third-party libraries under additional open source licenses.
- This project redistributes images from the [famfamfam/silk icon set](https://github.com/markjames/famfamfam-silk-icons) under the [CC-BY 2.5 license](http://creativecommons.org/licenses/by/2.5/).
- This project includes trademarked logo images for each supported database type.
- The Windows binary is released under LGPL-3+ owing to the static copy of Qt.
This project includes trademarked logo images for each supported database type. ## Download
Get the latest version from [the releases page »](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases)
## Changelog ## Changelog
2025-11-23 v0.8.0 See [the full change history »](https://git.ivysaur.me/code.ivysaur.me/qbolt/src/branch/master/CHANGELOG.md)
- Port from [liblcl](https://github.com/ying32/liblcl) to [MIQT Qt 6](https://github.com/mappu/miqt)
- Badger: Upgrade v4.2.0 -> v4.8.0
- Pebble: Upgrade v1.0.0 -> v1.1.5
- SQLite: Upgrade v1.14.22 -> v1.14.32
- Redis: Upgrade v9.5.3 -> v9.16.0
- Bolt: Upgrade v1.4.0-alpha.1 -> v1.4.3
- Bolt: Fix child buckets appearing in data area
- Badger, Pebble, Debconf: Remove redundant "Data" navigation layer
- Badger: Support encrypted databases
- Badger: Support readonly databases
- Badger: Add context-menu actions for backup, restore, and compact
- Pebble: Support readonly databases
- LevelDB: Add LevelDB database integration
- Redis: Support SSH tunnel
- SQLite: Allow editing the primary key column
- App: New style connection dialog
- App: Updated keyboard shortcuts (Ctrl+O to open new connection, F5 to refresh, F9 to execute query)
- App: Add confirmation when refreshing the data table if there are uncommitted changes
[⬇️ Download for Windows x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.8.0/yvbolt.win64.zip)
[⬇️ Download for Linux x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.8.0/yvbolt.linux64.tar.xz)
2024-07-18 v0.7.0
- SQLite, Bolt: Initial support for editing data (insert, per-cell update, delete)
- SQLite: Add context menu actions for compact (vacuum), export, and drop table
- App: New grid widget
- App: Add refresh button
- App: Bigger window size, use icons for toolbars, better UI colours for Windows
- App: Prevent submitting blank queries to database
- Refactor database interface and error handling
[⬇️ Download for Windows x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.7.0/yvbolt.win64.zip)
[⬇️ Download for Linux x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.7.0/yvbolt.linux64.tar.xz)
2024-06-30 v0.6.0
- Debconf: Add as supported database
- SQLite: Support table names containing special characters
- SQLite: Improvements for experimental command-line driver
- Redis: Improve connection dialog window position
- App: Cosmetic fixes for frame borders, help dialog, and Windows fonts+colours
- Build: Change compression parameters for release builds
- Build: Compile CGO with -O2 for release builds
[⬇️ Download for Windows x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.6.0/yvbolt.win64.zip)
[⬇️ Download for Linux x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.6.0/yvbolt.linux64.tar.xz)
2024-06-29 v0.5.0
- Pebble: Add as supported database
- Bolt: Support opening as readonly
- Bolt: Support creating new databases
- Bolt: Support adding/removing recursive child buckets
- SQLite: Support custom CLI driver that parses `/usr/bin/sqlite3 -json` output (experimental)
- Redis: Improve query parser to support quoted strings
- App: Support refreshing elements in nav tree
- App: Help menu option to show driver versions
- App: Add image icons for refresh and close context menu actions
- Build: Add makefile for cross-compiling release binaries
[⬇️ Download for Windows x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.5.0/yvbolt.win64.zip)
[⬇️ Download for Linux x86_64](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/download/v0.5.0/yvbolt.linux64.tar.xz)
2024-06-23 v0.4.0
- Redis: Add as supported database
- Badger: Allow creating in-memory databases
- App: Allow selecting partial query text to execute
- App: Allow closing database connections from context menu
- App: Allow scrolling large content on Properties pane
- App: Preload recursive navigation
- App: Automatically switch to selected database when new connection is created
- App: Add help website link
- App: Add database logo images
2024-06-25 v0.3.0
- Badger: Add BadgerDB v4 as supported database
- SQLite: Add support for CGo-free SQLite driver under cross-compilation
- Bolt: Update Bolt to v1.4.0-alpha.1
- App: Add support for running custom queries
- App: Add status bar showing currently selected DB
- App: Fix missing icons in nav when selecting items
- App: Fix extra quotemarks when browsing string content of database
2024-06-08 v0.2.0
- SQLite: Add SQLite support (now requires CGo)
- App: Add images for menu and navigation items
2024-06-03 v0.1.0
- Initial public release

95
TODO
View File

@@ -1,53 +1,114 @@
- BUG: Connecting to multiple DBs from connection manager: if one has an error, the error popup from the others is lost
- BUG: ExecQuery being called multiple times on error?
- Drag and drop database into UI (QBolt parity)
- Portable mode (portable.txt or portable/ dir)
- Syntax highlighting in editor - Syntax highlighting in editor
- Autorefresh
- Sshagent
- want to trigger an async refresh from inside the LDB after lock/unlock
- support adding/removing keys (will need per-row actions)
- SSH: knownhost parser is stricter than openssh, does not support hostname if there is known a knownhost for the IP address
- Bolt: import/export should support passworded zips
- Table: BSON view can't see data
- Table: quick filter
- QSortFilterProxyModel
- Cancellation
- Loading animations for connection + queries
- Mutation - Mutation
- Badger: Support insert/update/delete
- Pebble: Support insert/update/delete
- Debconf: Support insert/update/delete - Debconf: Support insert/update/delete
- Redis: Support insert/update/delete - Redis: Support insert/update/delete
- SecretService: Support insert/update/delete
- Binary data viewer - Binary data viewer
- Detect jpg/png and show as image - Detect jpg/png and show as image
- More DB types - More DB types
- MySQL - MySQL (& MariaDB/TiDB)
- Postgres - Postgres
- CLI using psql - CLI using psql
- Lungo: Mini embeddable Mongo - https://github.com/256dpi/lungo
- MSSQL (recursive navigation for instances) - MSSQL (recursive navigation for instances)
- Other K/V stores from https://github.com/smallnest/kvbench - Other K/V stores from https://github.com/smallnest/kvbench
- Windows registry - Windows registry
- Allow entering path for quick navigation
- LDAP - LDAP
- LotusDB https://github.com/lotusdblabs/lotusdb
- Bitcask https://git.mills.io/prologic/bitcask
- Related: Rosedb https://github.com/rosedblabs/rosedb
- Dolt - Dolt
- Memcache - Memcache
- Listing all keys is not well supported, needs hacks - Listing all keys is not well supported, needs hacks
- DBus SecretStore
- Starskey https://github.com/starskey-io/starskey
- Chai (built on Pebble) - https://github.com/chaisql/chai - Chai (built on Pebble) - https://github.com/chaisql/chai
- CloverDB (built on Bolt/Badger) - https://github.com/ostafen/clover
- APCu - need some sort of hook into the storage engine - APCu - need some sort of hook into the storage engine
- VoidDB - https://github.com/voidDB/voidDB
- UnisonDB - https://github.com/ankur-anand/unisondb
- Etcd - Etcd
- Connection dialog - v2: hierarchal
- Saved connections w/ storage encryption - v3: flat key namespace
- SSH over Cockpit - CSV file
- Allow querying with sqlite or duckDB?
- Parquet file
- Allow querying with duckDB?
- SSDB (Redis-compatible)
- Time-series DBs
- Prometheus
- VictoriaMetrics
- FrostDB https://github.com/polarsignals/frostdb
- https://dbdb.io/browse?programming=go-lang&q=
- Maxmind GeoIP MMDB format
- https://github.com/maxmind/mmdbwriter
- KeePass kdbx
- Not-quite-DBs
- IRC client
- Docker daemon (images, containers, ...)
- ssh-agent
- ssh known-hosts
- golang.org/x/crypto/ssh/knownhosts - already using this package
- Generic ODBC, database/sql, ...
- Other language DBs
- C, C++
- Tokyo Cabinet, Kyoto Cabinet, Tkrzw
- https://github.com/TerraTech/go-tokyocabinet needs pkg-config tokyocabinet
- https://github.com/estraier/tkrzw-go needs pkg-config tkrzw
- cdb (DJB's Constant Database)
- RocksDB
- https://github.com/tecbot/gorocksdb Go bindings, need pkg-config rocksdb
- Rust
- Stoolap https://github.com/stoolap/stoolap
- Rust, needs C binding layer https://github.com/mozilla/cbindgen
- JDBC Java databases
- H2, HSQLDB, Apache Derby
- SQLite CLI driver: - SQLite CLI driver:
- Context support - Context support
- Attach to SSH tunnel - Write support
- Configure binary path - Configure binary path
- Error handling: if an error occurs, listing db tables has problems/shows separators
- https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/ - https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/
- https://github.com/litements/litexplore - https://github.com/litements/litexplore
- Badger: - Badger:
- v1/v2/v3 support - v1/v2/v3 support
- option to use namespace separators for virtual buckets - option to use namespace separators for virtual buckets
- Debconf: separate groups by first slash in name - SQLite:
- SQLite: drop table doesn't autorefresh nav since callback is late - drop table doesn't autorefresh nav since callback is late
- Build - more accurate type handling
- Win32 icon resource - binary data currently shows as "<<binary>>", edits wrongly
- views
- other special objects (triggers? udf functions?)
- remove current hardcoded LIMIT 1000
- attach additional db to same connection
- LMDB: dupsort mode (duplicate keys / entries-per-key)
- MongoDB
- UI for replica sets, ssl certs, cluster, custom auth database
- SSH tunnel: error `ssh: tcpChan: deadline not supported` - needs workaround
- Backup/restore
- drop db/collection doesn't autorefresh nav since server is asynchronous
- SSH tunnel
- option to use external/system SSH
- SSH over Cockpit
- Performance - Performance
- Warning if data table is filtered to 1000 rows, or add pagination - Warning if data table is filtered to 1000 rows, or add pagination
- Context/interrupt slow queries - Context/interrupt slow queries
- Faster virtual rendering
- Query history - Query history
- Query log
- Test suite - Test suite
- `CREATE TABLE foo (id integer primary key, aaa text not null, bbb text not null);` - `CREATE TABLE foo (id integer primary key, aaa text not null, bbb text not null);`
- Ability to convert database types - Ability to convert database types
- Export all data from grid - Export all data from grid
- Export all data from all buckets within a DB - Export all data from all buckets within a DB
- Reconnect

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 B

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 685 B

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 637 B

BIN
assets/compress.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

BIN
assets/connect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 B

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 B

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 715 B

BIN
assets/database_key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 775 B

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 755 B

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 692 B

BIN
assets/disconnect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

BIN
assets/help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

BIN
assets/key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 739 B

After

Width:  |  Height:  |  Size: 770 B

BIN
assets/page_key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 B

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 B

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 666 B

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 B

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 566 B

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 B

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 723 B

After

Width:  |  Height:  |  Size: 744 B

BIN
assets/vendor_buntdb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 831 B

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 B

After

Width:  |  Height:  |  Size: 624 B

BIN
assets/vendor_lmdb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

BIN
assets/vendor_lotus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
assets/vendor_mongodb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 545 B

BIN
assets/vendor_qt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 825 B

BIN
assets/vendor_riak.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

BIN
assets/vendor_rosedb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 919 B

After

Width:  |  Height:  |  Size: 665 B

BIN
assets/vendor_ssh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

BIN
assets/vendor_starskey.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

418
config.go
View File

@@ -2,7 +2,11 @@ package main
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"os"
"reflect"
"time"
"github.com/mappu/autoconfig" "github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6" qt "github.com/mappu/miqt/qt6"
@@ -12,52 +16,107 @@ type DBConnector interface {
Connect(context.Context) (loadedDatabase, string, error) Connect(context.Context) (loadedDatabase, string, error)
} }
type registeredDatabase struct { type ConnectionConfig struct {
label string Type autoconfig.OneOf
iconPath string
conn DBConnector Badger *badgerConnection `ylabel:"BadgerDB v4" yicon:":/assets/vendor_dgraph.png" json:",omitempty"`
Bitcask *bitcaskDBConnection `yicon:":/assets/vendor_riak.png" json:",omitempty"`
Bolt *boltConfig `yicon:":/assets/vendor_github.png" json:",omitempty"`
BuntDB *buntDBConnection `ylabel:"BuntDB" yicon:":/assets/vendor_buntdb.png" json:",omitempty"`
Debconf *debconfConnection `yicon:":/assets/vendor_debian.png" json:",omitempty"`
SecretService *secretServiceConnection `ylabel:"Freedesktop.org Secret Service" yicon:":/assets/vendor_freedesktop.png" json:",omitempty"`
LevelDB *leveldbConnection `ylabel:"LevelDB" yicon:":/assets/vendor_leveldb.png" json:",omitempty"`
LMDB *lmdbConnection `yicon:":/assets/vendor_lmdb.png" json:",omitempty"`
LotusDB *lotusDBConnection `ylabel:"LotusDB" yicon:":/assets/vendor_lotus.png" json:",omitempty"`
MongoDB *mongoConnection `ylabel:"MongoDB" yicon:":/assets/vendor_mongodb.png" json:",omitempty"`
Pebble *pebbleConnection `yicon:":/assets/vendor_cockroach.png" json:",omitempty"`
Redis *redisConnectionOptions `yicon:":/assets/vendor_redis.png" json:",omitempty"`
RoseDB *roseDBConn `ylabel:"RoseDB" yicon:":/assets/vendor_rosedb.png" json:",omitempty"`
SQLite *sqliteConnection `ylabel:"SQLite" yicon:":/assets/vendor_sqlite.png" json:",omitempty"`
SSHAgent *sshAgentConn `yicon:":/assets/vendor_ssh.png" json:",omitempty"`
Starskey *starskeyConnection `yicon:":/assets/vendor_starskey.png" json:",omitempty"`
} }
var ( func NewConnectionConfig() *ConnectionConfig {
registeredDatabases []registeredDatabase return &ConnectionConfig{
) Type: "Bolt", // favouritism
}
func registerDatabase(conn DBConnector, label, iconPath string) {
registeredDatabases = append(registeredDatabases, registeredDatabase{
label: label,
iconPath: iconPath,
conn: conn,
})
} }
// Icon gets the Qt string name for the icon of this database by parsing the
// 'yicon' struct tag with reflection.
// If there is no selection or if it's misconfigured, falls back to a generic
// database icon.
func (cc *ConnectionConfig) Icon() string {
taginfo, ok := reflect.ValueOf(cc).Type().Elem().FieldByName(string(cc.Type))
if !ok {
return ":/assets/database.png"
}
yicon, ok := taginfo.Tag.Lookup("yicon")
if !ok {
return ":/assets/database.png"
}
return yicon
}
func (cc *ConnectionConfig) String() string {
if selection, err := cc.selection(); err == nil {
if stringer, ok := selection.(fmt.Stringer); ok {
return stringer.String()
}
}
if string(cc.Type) == "" {
return "Not configured"
}
return string(cc.Type)
}
func (cc *ConnectionConfig) selection() (DBConnector, error) {
selection := reflect.ValueOf(cc).Elem().FieldByName(string(cc.Type))
if !selection.IsValid() {
return nil, fmt.Errorf("Invalid database engine %q", cc.Type)
}
con, ok := selection.Interface().(DBConnector)
if !ok {
return nil, fmt.Errorf("Can't connect to database on type %q (weird)", cc.Type)
}
return con, nil
}
func (cc *ConnectionConfig) Connect(ctx context.Context) (loadedDatabase, string, error) {
dbc, err := cc.selection()
if err != nil {
return nil, "", fmt.Errorf("Invalid database engine %q", cc.Type)
}
return dbc.Connect(ctx)
}
var _ DBConnector = &ConnectionConfig{}
func (f *App) OnMnuConnectClick() { func (f *App) OnMnuConnectClick() {
config := NewConnectionConfig()
f.showConnectDialog(config)
}
func (f *App) showConnectDialog(config *ConnectionConfig) {
// autoconfig.OpenDialog() does not give us a "yes/no" result back from the
// dialog, so we still have to custom construct the dialog and use the
// formlayout option instead
dlg := NewConnectDialogUi() dlg := NewConnectDialogUi()
dlg.ConnectDialog.SetParent2(f.ui.MainWindow.QWidget, qt.Dialog) dlg.ConnectDialog.SetParent2(f.ui.MainWindow.QWidget, qt.Dialog)
dlg.ConnectDialog.SetModal(true) dlg.ConnectDialog.SetModal(true)
dlg.ConnectDialog.SetAttribute(qt.WA_DeleteOnClose) dlg.ConnectDialog.SetAttribute(qt.WA_DeleteOnClose)
var resultingConfigs []DBConnector saver := autoconfig.MakeConfigArea(config, dlg.formLayout)
var getters []func()
for _, db := range registeredDatabases {
dlg.databaseEngine.AddItem2(qt.NewQIcon4(db.iconPath), db.label)
frame := qt.NewQWidget2()
layout := qt.NewQFormLayout(frame)
// Deep-copy the provided default object
// useConn := deep_copy(db.conn)
useConn := db.conn // FIXME can't deep copy?!
resultingConfigs = append(resultingConfigs, useConn)
getters = append(getters, autoconfig.MakeConfigArea(useConn, layout))
dlg.stackedWidget.AddWidget(frame)
}
dlg.databaseEngine.OnCurrentIndexChanged(func(index int) {
dlg.stackedWidget.SetCurrentIndex(index)
})
dlg.ConnectDialog.OnAccept(func(super func()) { dlg.ConnectDialog.OnAccept(func(super func()) {
// Validate connection before closing // Validate connection before closing
@@ -66,18 +125,14 @@ func (f *App) OnMnuConnectClick() {
defer dlg.buttonBox.SetEnabled(true) defer dlg.buttonBox.SetEnabled(true)
dlg.buttonBox.Repaint() dlg.buttonBox.Repaint()
// Selected index
idx := dlg.databaseEngine.CurrentIndex()
// Save changes from UI into struct // Save changes from UI into struct
getters[idx]() saver()
config := resultingConfigs[idx]
// Connect -> get ld // Connect -> get ld
ctx := context.Background() // TODO do in background thread? ctx := context.Background() // TODO do in background thread?
ld, displayName, err := config.Connect(ctx) ld, displayName, err := config.Connect(ctx)
if err != nil { if err != nil {
_ = qt.QMessageBox_Critical(dlg.ConnectDialog.QWidget, APPNAME, fmt.Sprintf("Connecting to %s database: %s", registeredDatabases[idx].label, err.Error())) _ = qt.QMessageBox_Critical(dlg.ConnectDialog.QWidget, APPNAME, fmt.Sprintf("Connecting to %s database: %s", config.Type, err.Error()))
// Prevent the dialog from closing: do not call super() // Prevent the dialog from closing: do not call super()
return return
} }
@@ -85,6 +140,12 @@ func (f *App) OnMnuConnectClick() {
// Add ld to mainwindow // Add ld to mainwindow
f.addTopLevelDatabaseConnection(ld, displayName) f.addTopLevelDatabaseConnection(ld, displayName)
// Connection OK
// Offer to save into connection-manager
if res := qt.QMessageBox_Question2(dlg.ConnectDialog.QWidget, APPNAME, "Connection successful. Save the details into Connection Manager?", qt.QMessageBox__Save, qt.QMessageBox__Ignore); res == int(qt.QMessageBox__Save) {
f.TrySaveIntoConnectionManager(config, displayName)
}
// Default accept behaviour is: setResult(Accepted), emits onFinished; && Hide() // Default accept behaviour is: setResult(Accepted), emits onFinished; && Hide()
super() super()
}) })
@@ -92,3 +153,276 @@ func (f *App) OnMnuConnectClick() {
dlg.ConnectDialog.Open() // Modal, unlike .Show() dlg.ConnectDialog.Open() // Modal, unlike .Show()
} }
type ConnMgrLoadError struct {
e error
}
func (c ConnMgrLoadError) Error() string {
return fmt.Sprintf("Failed to load saved connections: %s", c.e)
}
func (c ConnMgrLoadError) Unwrap() error {
return c.e
}
type ConnMgrSaveError struct {
e error
}
func (c ConnMgrSaveError) Error() string {
return fmt.Sprintf("Failed to save connections: %s", c.e)
}
func (c ConnMgrSaveError) Unwrap() error {
return c.e
}
func (f *App) TrySaveIntoConnectionManager(cc *ConnectionConfig, displayName string) {
data, err := f.getConnectionManagerContents()
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrLoadError{err}.Error())
}
if displayName == "" {
displayName = cc.String()
}
data.Entries = append(data.Entries, SavedConfigEntry{
Description: displayName,
Connection: *cc,
})
err = f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
}
}
func (f *App) OnMnuConnectionManagerClick() {
data, err := f.getConnectionManagerContents()
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
dlg := NewconnectionManagerDialogUi()
dlg.connectionManagerDialog.SetParent2(f.ui.MainWindow.QWidget, qt.Dialog)
dlg.connectionManagerDialog.SetModal(true)
dlg.connectionManagerDialog.SetAttribute(qt.WA_DeleteOnClose)
dlg.treeWidget.SetRootIsDecorated(false)
dlg.treeWidget.SetSelectionMode(qt.QAbstractItemView__ExtendedSelection)
addEntryFor := func(entry SavedConfigEntry) {
itm := qt.NewQTreeWidgetItem()
itm.SetText(0, entry.Description)
itm.SetIcon(0, qt.NewQIcon4(entry.Connection.Icon()))
dlg.treeWidget.AddTopLevelItem(itm)
}
refreshEntryFor := func(idx *qt.QModelIndex, entry SavedConfigEntry) {
itm := dlg.treeWidget.ItemFromIndex(idx)
itm.SetText(0, entry.Description) // Update label
itm.SetIcon(0, qt.NewQIcon4(entry.Connection.Icon())) // Update icon
}
// Populate entries
for _, entry := range data.Entries {
addEntryFor(entry)
}
refreshButtonState := func() {
ct := len(dlg.treeWidget.SelectedItems())
dlg.connectBtn.SetEnabled(ct > 0)
dlg.connDelete.SetEnabled(ct > 0)
dlg.connEdit.SetEnabled(ct == 1)
}
connectToItem := func(itm *qt.QTreeWidgetItem) bool {
ctx := context.Background() // TODO do in background thread?
dlg.connectBtn.SetEnabled(false)
dlg.treeWidget.SetEnabled(false)
defer func() {
dlg.connectBtn.SetEnabled(true)
dlg.treeWidget.SetEnabled(true) // FIXME block the other buttons too!
}()
entry := data.Entries[dlg.treeWidget.IndexFromItem(itm).Row()]
ld, _, err := entry.Connection.Connect(ctx)
if err != nil {
_ = qt.QMessageBox_Critical(dlg.connectionManagerDialog.QWidget, APPNAME, fmt.Sprintf("Connecting to %s database: %s", entry.Connection.Type, err.Error()))
return false
}
// Add ld to mainwindow
// Don't use the displayName from the Connect() function - use the saved
// displayname instead
f.addTopLevelDatabaseConnection(ld, entry.Description)
return true
}
dlg.treeWidget.OnItemDoubleClicked(func(itm *qt.QTreeWidgetItem, _ int) {
// connect to specific item
if connectToItem(itm) {
dlg.connectionManagerDialog.Accept()
}
})
dlg.connectBtn.OnClicked(func() {
// Based on selectedItems, not currentItem
itms := dlg.treeWidget.SelectedItems()
var ok bool = true
for _, itm := range itms {
ok = ok && connectToItem(itm) // connect to selected
}
if ok {
dlg.connectionManagerDialog.Accept()
}
})
dlg.connDelete.SetEnabled(false)
dlg.connEdit.SetEnabled(false)
dlg.treeWidget.OnItemSelectionChanged(refreshButtonState)
dlg.connDelete.OnClicked(func() {
itms := dlg.treeWidget.SelectedItems()
for _, itm := range itms {
idx := dlg.treeWidget.IndexFromItem(itm).Row() // n.b. will dynamically change as the item is removed
itm.Delete()
data.Entries = slice_remove_index(data.Entries, idx)
}
// All changes made in both ui + data model
// Save data model changes
err := f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
dlg.connEdit.OnClicked(func() {
// Edit is based on selectedIndex, not currentIndex
items := dlg.treeWidget.SelectedItems()
if len(items) != 1 {
return
}
idx := dlg.treeWidget.IndexFromItem(items[0])
row := idx.Row()
props := data.Entries[row]
autoconfig.OpenDialog(&props, dlg.connectionManagerDialog.QWidget, "Editing connection", func() {
data.Entries[row] = props
refreshEntryFor(idx, props)
err := f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
})
dlg.connAdd.OnClicked(func() {
obj := SavedConfigEntry{
Description: "New connection (" + time.Now().Format(time.DateTime) + ")",
}
vv := NewConnectionConfig()
obj.Connection = *vv
autoconfig.OpenDialog(&obj, dlg.connectionManagerDialog.QWidget, "New connection", func() {
data.Entries = append(data.Entries, obj)
addEntryFor(obj)
err := f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
})
miniMenu := qt.NewQMenu(dlg.toolsBtn.QWidget)
exportAction := miniMenu.AddActionWithText("Export connections...")
importAction := miniMenu.AddActionWithText("Import connections...")
dlg.toolsBtn.SetMenu(miniMenu)
dlg.toolsBtn.SetPopupMode(qt.QToolButton__InstantPopup)
exportAction.OnTriggered(func() {
saveAs := qt.QFileDialog_GetSaveFileName4(dlg.toolsBtn.QWidget, "Export connections...", "", "JSON files (*.json);;All files (*)")
if saveAs == "" {
return // cancelled
}
jbytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
err = os.WriteFile(saveAs, jbytes, 0600)
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
// OK
qt.QMessageBox_Information(dlg.toolsBtn.QWidget, APPNAME, "The connections have been exported successfully.")
})
importAction.OnTriggered(func() {
loadFile := qt.QFileDialog_GetOpenFileName4(dlg.toolsBtn.QWidget, "Import connections...", "", "JSON files (*.json);;All files (*)")
if loadFile == "" {
return // cancelled
}
jbytes, err := os.ReadFile(loadFile)
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrLoadError{err}.Error())
return
}
var importData SavedConfig
err = json.Unmarshal(jbytes, &importData)
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrLoadError{err}.Error())
return
}
// Add to UI
for _, newEntry := range importData.Entries {
addEntryFor(newEntry)
}
// Add to data model
data.Entries = append(data.Entries, importData.Entries...)
// Save data model to disk
err = f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
refreshButtonState()
dlg.connectionManagerDialog.Show()
}

198
configSave.go Normal file
View File

@@ -0,0 +1,198 @@
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/zalando/go-keyring"
)
type SavedConfigEntry struct {
Description string
Connection ConnectionConfig
}
type SavedConfig struct {
UserAgent string // APPNAME/{ver}
Entries []SavedConfigEntry
}
const (
saveSettingsFilename = `settings.dat`
keychainUserName = `settings-encryption-key`
)
func (f *App) getConnectionManagerContents() (*SavedConfig, error) {
cfgd, err := os.UserConfigDir()
if err != nil {
return nil, err
}
_ = os.MkdirAll(filepath.Join(cfgd, APPNAME), 0700)
ciphertext, err := os.ReadFile(filepath.Join(cfgd, APPNAME, saveSettingsFilename))
if err != nil {
if os.IsNotExist(err) {
// No file exists. Use blank
return &SavedConfig{
UserAgent: APPNAME,
}, nil
}
return nil, err
}
// Get decryption key from OS keychain
// Since we successfully loaded the file on disk, the keychain entry must exist
details, err := keyring.Get(APPNAME, keychainUserName)
if err != nil {
return nil, err
}
// Decrypt
// The encryption key is random 256 bytes, no need for a KDF
encryptionKeyBytes, err := hex.DecodeString(details)
if err != nil {
return nil, err
}
cc, err := aes.NewCipher(encryptionKeyBytes)
if err != nil {
return nil, err
}
cw, err := cipher.NewGCMWithRandomNonce(cc)
if err != nil {
return nil, err
}
plaintext, err := cw.Open(ciphertext[:0], nil, ciphertext, nil) // @ref https://pkg.go.dev/crypto/cipher#AEAD
if err != nil {
return nil, err
}
var ret SavedConfig
err = json.Unmarshal(plaintext, &ret)
if err != nil {
return nil, err
}
// Before returning the configuration, sort the entries alphabetically
sort.Slice(ret.Entries, func(i, j int) bool {
// FIXME there is probably a slightly more efficient way of doing this
return strings.ToLower(ret.Entries[i].Description) < strings.ToLower(ret.Entries[j].Description)
})
// Success
return &ret, nil
}
func (f *App) saveConnectionManagerContents(sc *SavedConfig) error {
cfgd, err := os.UserConfigDir()
if err != nil {
return err
}
// If the file exists already, we must have an existing encryption key
var exists bool
savePath := filepath.Join(cfgd, APPNAME, saveSettingsFilename)
if _, err := os.Stat(savePath); err != nil {
if os.IsNotExist(err) {
_ = os.MkdirAll(filepath.Join(cfgd, APPNAME), 0700)
exists = false
} else {
return err // some other real error
}
} else {
exists = true
}
details, err := keyring.Get(APPNAME, keychainUserName)
if err != nil {
if errors.Is(err, keyring.ErrNotFound) {
// Does not exist in keyring
// That is OK if there is no saved file yet / we can generate a random
// key. But if the file does already exist, this is fatal
if exists {
// File exists on disk but there is no encryption key to parse it
// Fatal
return fmt.Errorf(
"There is already a saved file on disk (%q), but there is no matching encryption key in the system keychain provider. Has your keychain been destroyed? To confirm, please delete the saved settings file.",
savePath,
)
} else {
// Generate new
// 256 bits (32 bytes) of cryto-random data, and then hex encode
randBuff := make([]byte, 32)
n, err := rand.Read(randBuff)
if err != nil {
return err
}
if n != len(randBuff) {
return io.ErrShortWrite
}
details = hex.EncodeToString(randBuff)
err = keyring.Set(APPNAME, keychainUserName, details)
if err != nil {
return err
}
// Safe
}
} else {
// Real keychain error
return err
}
}
// Force update the saved version
sc.UserAgent = APPNAME + `/` + appVersion // e.g. QBolt/v0.0.0-devel
// Marshal
plaintext, err := json.Marshal(sc)
if err != nil {
return err
}
// Encrypt
encryptionKeyBytes, err := hex.DecodeString(details)
if err != nil {
return err
}
cc, err := aes.NewCipher(encryptionKeyBytes)
if err != nil {
return err
}
cw, err := cipher.NewGCMWithRandomNonce(cc)
if err != nil {
return err
}
ciphertext := cw.Seal(plaintext[:0], nil, plaintext, nil)
// Save to disk
err = os.WriteFile(savePath, ciphertext, 0600)
if err != nil {
return err
}
// Save OK
return nil
}

View File

@@ -10,11 +10,11 @@ import (
) )
type ConnectDialogUi struct { type ConnectDialogUi struct {
ConnectDialog *qt.QDialog ConnectDialog *qt.QDialog
gridLayout *qt.QGridLayout gridLayout *qt.QGridLayout
databaseEngine *qt.QComboBox buttonBox *qt.QDialogButtonBox
stackedWidget *qt.QStackedWidget formWidget *qt.QWidget
buttonBox *qt.QDialogButtonBox formLayout *qt.QFormLayout
} }
// NewConnectDialogUi creates all Qt widget classes for ConnectDialog. // NewConnectDialogUi creates all Qt widget classes for ConnectDialog.
@@ -34,18 +34,6 @@ func NewConnectDialogUi() *ConnectDialogUi {
gridLayout__objectName.Delete() // setter copied value gridLayout__objectName.Delete() // setter copied value
ui.gridLayout.SetContentsMargins(11, 11, 11, 11) ui.gridLayout.SetContentsMargins(11, 11, 11, 11)
ui.gridLayout.SetSpacing(6) ui.gridLayout.SetSpacing(6)
ui.databaseEngine = qt.NewQComboBox(ui.ConnectDialog.QWidget)
databaseEngine__objectName := qt.NewQAnyStringView3("databaseEngine")
ui.databaseEngine.SetObjectName(*databaseEngine__objectName)
databaseEngine__objectName.Delete() // setter copied value
ui.gridLayout.AddWidget2(ui.databaseEngine.QWidget, 0, 1)
ui.stackedWidget = qt.NewQStackedWidget(ui.ConnectDialog.QWidget)
stackedWidget__objectName := qt.NewQAnyStringView3("stackedWidget")
ui.stackedWidget.SetObjectName(*stackedWidget__objectName)
stackedWidget__objectName.Delete() // setter copied value
ui.gridLayout.AddWidget2(ui.stackedWidget.QWidget, 1, 1)
ui.buttonBox = qt.NewQDialogButtonBox(ui.ConnectDialog.QWidget) ui.buttonBox = qt.NewQDialogButtonBox(ui.ConnectDialog.QWidget)
buttonBox__objectName := qt.NewQAnyStringView3("buttonBox") buttonBox__objectName := qt.NewQAnyStringView3("buttonBox")
ui.buttonBox.SetObjectName(*buttonBox__objectName) ui.buttonBox.SetObjectName(*buttonBox__objectName)
@@ -53,7 +41,19 @@ func NewConnectDialogUi() *ConnectDialogUi {
ui.buttonBox.SetOrientation(qt.Horizontal) ui.buttonBox.SetOrientation(qt.Horizontal)
ui.buttonBox.SetStandardButtons(qt.QDialogButtonBox__Cancel | qt.QDialogButtonBox__Ok) ui.buttonBox.SetStandardButtons(qt.QDialogButtonBox__Cancel | qt.QDialogButtonBox__Ok)
ui.gridLayout.AddWidget3(ui.buttonBox.QWidget, 2, 0, 1, 2) ui.gridLayout.AddWidget2(ui.buttonBox.QWidget, 2, 0)
ui.formWidget = qt.NewQWidget(ui.ConnectDialog.QWidget)
formWidget__objectName := qt.NewQAnyStringView3("formWidget")
ui.formWidget.SetObjectName(*formWidget__objectName)
formWidget__objectName.Delete() // setter copied value
ui.formLayout = qt.NewQFormLayout(ui.formWidget)
formLayout__objectName := qt.NewQAnyStringView3("formLayout")
ui.formLayout.SetObjectName(*formLayout__objectName)
formLayout__objectName.Delete() // setter copied value
ui.formLayout.SetContentsMargins(0, 0, 0, 11)
ui.formLayout.SetSpacing(6)
ui.gridLayout.AddWidget2(ui.formWidget, 1, 0)
ui.buttonBox.OnAccepted(ui.ConnectDialog.Accept) ui.buttonBox.OnAccepted(ui.ConnectDialog.Accept)
ui.buttonBox.OnRejected(ui.ConnectDialog.Reject) ui.buttonBox.OnRejected(ui.ConnectDialog.Reject)

View File

@@ -18,13 +18,7 @@
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset> <normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="1"> <item row="2" column="0">
<widget class="QComboBox" name="databaseEngine"/>
</item>
<item row="1" column="1">
<widget class="QStackedWidget" name="stackedWidget"/>
</item>
<item row="2" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
@@ -34,6 +28,21 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0">
<widget class="QWidget" name="formWidget" native="true">
<layout class="QFormLayout" name="formLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources> <resources>

136
connectionManagerDialog.go Normal file
View File

@@ -0,0 +1,136 @@
// 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 connectionManagerDialog.ui -OutFile connectionManagerDialog.go -Qt6
package main
import (
qt "github.com/mappu/miqt/qt6"
)
type connectionManagerDialogUi struct {
connectionManagerDialog *qt.QDialog
verticalLayout_2 *qt.QVBoxLayout
horizontalLayout *qt.QHBoxLayout
treeWidget *qt.QTreeWidget
verticalLayout *qt.QVBoxLayout
connAdd *qt.QToolButton
connEdit *qt.QToolButton
connDelete *qt.QToolButton
verticalSpacer *qt.QSpacerItem
horizontalLayout_2 *qt.QHBoxLayout
toolsBtn *qt.QToolButton
horizontalSpacer *qt.QSpacerItem
connectBtn *qt.QPushButton
}
// NewconnectionManagerDialogUi creates all Qt widget classes for connectionManagerDialog.
func NewconnectionManagerDialogUi() *connectionManagerDialogUi {
ui := &connectionManagerDialogUi{}
ui.connectionManagerDialog = qt.NewQDialog(nil)
connectionManagerDialog__objectName := qt.NewQAnyStringView3("connectionManagerDialog")
ui.connectionManagerDialog.SetObjectName(*connectionManagerDialog__objectName)
connectionManagerDialog__objectName.Delete() // setter copied value
ui.connectionManagerDialog.Resize(332, 449)
ui.verticalLayout_2 = qt.NewQVBoxLayout(ui.connectionManagerDialog.QWidget)
verticalLayout_2__objectName := qt.NewQAnyStringView3("verticalLayout_2")
ui.verticalLayout_2.SetObjectName(*verticalLayout_2__objectName)
verticalLayout_2__objectName.Delete() // setter copied value
ui.verticalLayout_2.SetContentsMargins(11, 11, 11, 11)
ui.verticalLayout_2.SetSpacing(6)
ui.horizontalLayout = qt.NewQHBoxLayout2()
horizontalLayout__objectName := qt.NewQAnyStringView3("horizontalLayout")
ui.horizontalLayout.SetObjectName(*horizontalLayout__objectName)
horizontalLayout__objectName.Delete() // setter copied value
ui.horizontalLayout.SetContentsMargins(0, 0, 0, 0)
ui.horizontalLayout.SetSpacing(6)
ui.treeWidget = qt.NewQTreeWidget(ui.connectionManagerDialog.QWidget)
treeWidget__objectName := qt.NewQAnyStringView3("treeWidget")
ui.treeWidget.SetObjectName(*treeWidget__objectName)
treeWidget__objectName.Delete() // setter copied value
ui.horizontalLayout.AddWidget(ui.treeWidget.QWidget)
ui.verticalLayout = qt.NewQVBoxLayout2()
verticalLayout__objectName := qt.NewQAnyStringView3("verticalLayout")
ui.verticalLayout.SetObjectName(*verticalLayout__objectName)
verticalLayout__objectName.Delete() // setter copied value
ui.verticalLayout.SetContentsMargins(0, 0, 0, 0)
ui.verticalLayout.SetSpacing(6)
ui.connAdd = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
connAdd__objectName := qt.NewQAnyStringView3("connAdd")
ui.connAdd.SetObjectName(*connAdd__objectName)
connAdd__objectName.Delete() // setter copied value
icon0 := qt.NewQIcon()
icon0.AddFile4(":/assets/add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connAdd.SetIcon(icon0)
ui.connAdd.SetAutoRaise(true)
ui.verticalLayout.AddWidget(ui.connAdd.QWidget)
ui.connEdit = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
connEdit__objectName := qt.NewQAnyStringView3("connEdit")
ui.connEdit.SetObjectName(*connEdit__objectName)
connEdit__objectName.Delete() // setter copied value
icon1 := qt.NewQIcon()
icon1.AddFile4(":/assets/pencil.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connEdit.SetIcon(icon1)
ui.connEdit.SetAutoRaise(true)
ui.verticalLayout.AddWidget(ui.connEdit.QWidget)
ui.connDelete = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
connDelete__objectName := qt.NewQAnyStringView3("connDelete")
ui.connDelete.SetObjectName(*connDelete__objectName)
connDelete__objectName.Delete() // setter copied value
icon2 := qt.NewQIcon()
icon2.AddFile4(":/assets/delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connDelete.SetIcon(icon2)
ui.connDelete.SetAutoRaise(true)
ui.verticalLayout.AddWidget(ui.connDelete.QWidget)
ui.verticalSpacer = qt.NewQSpacerItem4(20, 40, qt.QSizePolicy__Minimum, qt.QSizePolicy__Expanding)
ui.verticalLayout.AddItem(ui.verticalSpacer.QLayoutItem)
ui.horizontalLayout.AddLayout(ui.verticalLayout.QLayout)
ui.verticalLayout_2.AddLayout(ui.horizontalLayout.QLayout)
ui.horizontalLayout_2 = qt.NewQHBoxLayout2()
horizontalLayout_2__objectName := qt.NewQAnyStringView3("horizontalLayout_2")
ui.horizontalLayout_2.SetObjectName(*horizontalLayout_2__objectName)
horizontalLayout_2__objectName.Delete() // setter copied value
ui.horizontalLayout_2.SetContentsMargins(0, 0, 0, 0)
ui.horizontalLayout_2.SetSpacing(6)
ui.toolsBtn = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
toolsBtn__objectName := qt.NewQAnyStringView3("toolsBtn")
ui.toolsBtn.SetObjectName(*toolsBtn__objectName)
toolsBtn__objectName.Delete() // setter copied value
ui.toolsBtn.SetAutoRaise(true)
ui.horizontalLayout_2.AddWidget(ui.toolsBtn.QWidget)
ui.horizontalSpacer = qt.NewQSpacerItem4(40, 20, qt.QSizePolicy__Expanding, qt.QSizePolicy__Minimum)
ui.horizontalLayout_2.AddItem(ui.horizontalSpacer.QLayoutItem)
ui.connectBtn = qt.NewQPushButton(ui.connectionManagerDialog.QWidget)
connectBtn__objectName := qt.NewQAnyStringView3("connectBtn")
ui.connectBtn.SetObjectName(*connectBtn__objectName)
connectBtn__objectName.Delete() // setter copied value
icon3 := qt.NewQIcon()
icon3.AddFile4(":/assets/database_lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connectBtn.SetIcon(icon3)
ui.horizontalLayout_2.AddWidget(ui.connectBtn.QWidget)
ui.verticalLayout_2.AddLayout(ui.horizontalLayout_2.QLayout)
ui.Retranslate()
return ui
}
// Retranslate reapplies all text translations.
func (ui *connectionManagerDialogUi) Retranslate() {
ui.connectionManagerDialog.SetWindowTitle(qt.QCoreApplication_Tr("Connection Manager"))
ui.treeWidget.HeaderItem().SetText(0, qt.QTreeWidget_Tr("Connection"))
ui.toolsBtn.SetText(qt.QDialog_Tr("Tools"))
ui.connectBtn.SetText(qt.QDialog_Tr("Connect"))
}

124
connectionManagerDialog.ui Normal file
View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>connectionManagerDialog</class>
<widget class="QDialog" name="connectionManagerDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>332</width>
<height>449</height>
</rect>
</property>
<property name="windowTitle">
<string>Connection Manager</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTreeWidget" name="treeWidget">
<column>
<property name="text">
<string>Connection</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QToolButton" name="connAdd">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/add.png</normaloff>:/assets/add.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="connEdit">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/pencil.png</normaloff>:/assets/pencil.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="connDelete">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/delete.png</normaloff>:/assets/delete.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QToolButton" name="toolsBtn">
<property name="text">
<string>Tools</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="connectBtn">
<property name="text">
<string>Connect</string>
</property>
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_lightning.png</normaloff>:/assets/database_lightning.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="embed.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -21,20 +21,17 @@ func (ld *badgerLoadedDatabase) DriverName() string {
return "BadgerDB v4" return "BadgerDB v4"
} }
func (ld *badgerLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string) error { func (ld *badgerLoadedDatabase) Properties(bucketPath []string) (string, error) {
// Load properties
content := fmt.Sprintf("Table statistics: %#v", ld.db.Tables()) content := fmt.Sprintf("Table statistics: %#v", ld.db.Tables())
f.propertiesBox.SetText(content) return content, nil
}
func (ld *badgerLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load data // Load data
// Badger always uses Key + Value as the columns // Badger always uses Key + Value as the columns
f.contentBox.SetColumnCount(2) f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"})
f.contentBox.SetHorizontalHeaderLabels([]string{"Key", "Value"})
f.contentBox.SetRowCount(0)
err := ld.db.View(func(txn *badger.Txn) error { err := ld.db.View(func(txn *badger.Txn) error {
@@ -48,11 +45,7 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []strin
item := it.Item() item := it.Item()
k := item.Key() k := item.Key()
err := item.Value(func(v []byte) error { err := item.Value(func(v []byte) error {
rpos := f.contentBox.RowCount() f.AddRow_PK_Data(k, k, v)
f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetItem(rpos, 0, qt.NewQTableWidgetItem2(formatUtf8(k)))
f.contentBox.SetItem(rpos, 0, qt.NewQTableWidgetItem2(formatUtf8(v)))
return nil return nil
}) })
if err != nil { if err != nil {
@@ -66,11 +59,16 @@ func (ld *badgerLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []strin
} }
// Valid // Valid
f.contentBox.ResizeColumnsToContents() f.Ready()
f.contentBox.SetEnabled(true)
return nil return nil
} }
func (n *badgerLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
return n.db.Update(func(txn *badger.Txn) error {
return kvstore_ApplyChanges(f, txn.Set, txn.Delete)
})
}
func (ld *badgerLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) { func (ld *badgerLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children return []string{}, nil // No children
} }
@@ -84,7 +82,7 @@ func (ld *badgerLoadedDatabase) NavContext(bucketPath []string) ([]contextAction
} }
func (ld *badgerLoadedDatabase) ExportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error { func (ld *badgerLoadedDatabase) ExportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error {
saveAs := qt.QFileDialog_GetSaveFileName4(sender.TreeWidget().QWidget, "Export backup...", "", "Badger database backups (*.bak);;All files (*.*)") saveAs := qt.QFileDialog_GetSaveFileName4(sender.TreeWidget().QWidget, "Export backup...", "", "Badger database backups (*.bak);;All files (*)")
if saveAs == "" { if saveAs == "" {
return nil return nil
} }
@@ -100,7 +98,7 @@ func (ld *badgerLoadedDatabase) ExportBackup(sender *qt.QTreeWidgetItem, bucketP
} }
func (ld *badgerLoadedDatabase) ImportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error { func (ld *badgerLoadedDatabase) ImportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error {
openPath := qt.QFileDialog_GetOpenFileName4(sender.TreeWidget().QWidget, "Import backup...", "", "Badger database backups (*.bak);;All files (*.*)") openPath := qt.QFileDialog_GetOpenFileName4(sender.TreeWidget().QWidget, "Import backup...", "", "Badger database backups (*.bak);;All files (*)")
if openPath == "" { if openPath == "" {
return nil return nil
} }
@@ -134,63 +132,77 @@ func (ld *badgerLoadedDatabase) Close() {
_ = ld.db.Close() _ = ld.db.Close()
} }
var _ loadedDatabase = &badgerLoadedDatabase{} // interface assertion var _ loadedDatabase = &badgerLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &badgerLoadedDatabase{} // interface assertion
// //
type badgerMemConnection struct{} type badgerConnection struct {
Type autoconfig.OneOf
Disk *struct {
Directory autoconfig.ExistingDirectory
Readonly bool
Encryption *encryptionKey
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
}
func (bmc *badgerMemConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { type encryptionKey struct {
db, err := badger.Open(badger.DefaultOptions("").WithInMemory(true)) Method autoconfig.EnumList `yenum:"Text;;Hex;;Passphrase (SHA256 KDF to AES-256)"`
if err != nil { Key autoconfig.Password
return nil, "", err }
func (e encryptionKey) Get() ([]byte, error) {
switch e.Method {
case 0: // Text
return []byte(e.Key), nil
case 1: // Hex
// For Badger, the input must be 16/24/32 bytes for AES-128/192/256
// The library checks this, we don't need to
return hex.DecodeString(string(e.Key))
case 2: // Passphrase (SHA256 KDF to AES-256)
hasher := sha256.New()
hasher.Write([]byte(e.Key))
return hasher.Sum(nil), nil
default:
return nil, fmt.Errorf("Unsupported encoding method for encryption key")
} }
return &badgerLoadedDatabase{db: db}, `:memory:`, nil // SQLite-style naming
} }
// func (bdc *badgerConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
if bdc.Disk != nil {
opts := badger.DefaultOptions(string(bdc.Disk.Directory))
opts.ReadOnly = bdc.Disk.Readonly
opts.MetricsEnabled = false
type badgerDirConnection struct { if bdc.Disk.Encryption != nil {
Directory autoconfig.ExistingDirectory ehx, err := bdc.Disk.Encryption.Get()
Readonly bool if err != nil {
Encryption_Method autoconfig.EnumList `yenum:"None;;Hex (AES-128/192/256);;Passphrase (SHA256 KDF to AES-256)"` return nil, "", fmt.Errorf("Loading encryption key: %w", err)
Encryption_Key autoconfig.Password }
}
func (bdc *badgerDirConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { if !(len(ehx) == 16 || len(ehx) == 24 || len(ehx) == 32) {
opts := badger.DefaultOptions(string(bdc.Directory)) return nil, "", fmt.Errorf("Encryption key must be 16/24/32 bytes long, got %d", len(ehx))
opts.ReadOnly = bdc.Readonly }
opts.MetricsEnabled = false opts.EncryptionKey = ehx
}
switch bdc.Encryption_Method { db, err := badger.Open(opts)
case 0: // None
case 1: // Hex (AES-128/192/256) - must be 16/24/32 bytes
ehx, err := hex.DecodeString(string(bdc.Encryption_Key))
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
if !(len(ehx) == 16 || len(ehx) == 24 || len(ehx) == 32) { return &badgerLoadedDatabase{db: db}, filepath.Base(string(bdc.Disk.Directory)), nil
return nil, "", fmt.Errorf("Hex encryption key must be 16/24/32 bytes long, got %d", len(ehx)) } else { // memory
db, err := badger.Open(badger.DefaultOptions("").WithInMemory(true))
if err != nil {
return nil, "", err
} }
opts.EncryptionKey = ehx
case 2: // Passphrase (SHA256 KDF to AES-256) return &badgerLoadedDatabase{db: db}, `:memory:`, nil // SQLite-style naming
hasher := sha256.New()
hasher.Write([]byte(bdc.Encryption_Key))
opts.EncryptionKey = hasher.Sum(nil)
} }
db, err := badger.Open(opts)
if err != nil {
return nil, "", err
}
return &badgerLoadedDatabase{db: db}, filepath.Base(string(bdc.Directory)), nil
}
func init() {
registerDatabase(&badgerMemConnection{}, "BadgerDB v4 (Memory)", `:/assets/vendor_dgraph.png`)
registerDatabase(&badgerDirConnection{}, "BadgerDB v4", `:/assets/vendor_dgraph.png`)
} }

113
db_bitcask.go Normal file
View File

@@ -0,0 +1,113 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"go.mills.io/bitcask/v2"
)
type bitcaskLdb struct {
db *bitcask.Bitcask
}
func (ld *bitcaskLdb) DriverName() string {
return "Bitcask"
}
func (ld *bitcaskLdb) Properties(bucketPath []string) (string, error) {
stats, err := ld.db.Stats()
if err != nil {
return "", err
}
return fmt.Sprintf("Database: %s\n\nStats:\n%#v\n", ld.db.Path(), stats), nil
}
func (ld *bitcaskLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"})
cur := ld.db.Iterator()
defer cur.Close()
for {
itm, err := cur.Next()
if err != nil {
if err == bitcask.ErrStopIteration {
break // OK
}
return err
}
f.AddRow_PK_Data([]byte(itm.Key()), []byte(itm.Key()), []byte(itm.Value()))
}
// Valid
f.Ready()
return nil
}
func (n *bitcaskLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return kvstore_ApplyChanges(
f,
func(k, v []byte) error { return n.db.Put(bitcask.Key(k), bitcask.Value(v)) },
func(k []byte) error { return n.db.Delete(bitcask.Key(k)) },
)
}
func (ld *bitcaskLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *bitcaskLdb) actionBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error {
backupDir := qt.QFileDialog_GetExistingDirectory3(sender.TreeWidget().QWidget, APPNAME, "Select an output directory to backup to...")
if backupDir == "" {
return nil // cancelled
}
return ld.db.Backup(backupDir)
}
func (ld *bitcaskLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{"Backup...", ld.actionBackup},
}, nil
}
func (ld *bitcaskLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &bitcaskLdb{} // interface assertion
var _ editableLoadedDatabase = &bitcaskLdb{} // interface assertion
//
type bitcaskDBConnection struct {
Directory autoconfig.ExistingDirectory
Readonly bool
AutoRecovery bool
}
func (c *bitcaskDBConnection) Reset() {
c.AutoRecovery = true
}
func (c *bitcaskDBConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
db, err := bitcask.Open(
string(c.Directory),
bitcask.WithOpenReadonly(c.Readonly),
bitcask.WithAutoRecovery(c.AutoRecovery),
)
if err != nil {
return nil, "", err
}
return &bitcaskLdb{db: db}, filepath.Base(string(c.Directory)), nil
}

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
@@ -21,10 +22,14 @@ type boltLoadedDatabase struct {
} }
type boltConfig struct { type boltConfig struct {
Path autoconfig.ExistingFile `yfilter:"Bolt database (*.db);;All files (*.*)"` Path autoconfig.ExistingFile `yfilter:"Bolt database (*.db);;All files (*)"`
Readonly bool Readonly bool
} }
func (bc *boltConfig) String() string { // n.b. only used for default names in connection manager
return filepath.Base(string(bc.Path))
}
func (bc *boltConfig) Connect(ctx context.Context) (loadedDatabase, string, error) { func (bc *boltConfig) Connect(ctx context.Context) (loadedDatabase, string, error) {
opts := bbolt.Options{ opts := bbolt.Options{
@@ -50,21 +55,19 @@ func (bc *boltConfig) Connect(ctx context.Context) (loadedDatabase, string, erro
return ld, displayName, nil return ld, displayName, nil
} }
func init() {
registerDatabase(&boltConfig{}, "Bolt", ":/assets/vendor_github.png")
}
func (ld *boltLoadedDatabase) DriverName() string { func (ld *boltLoadedDatabase) DriverName() string {
return "Bolt " + version.Version return "Bolt " + version.Version
} }
func (ld *boltLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string) error { func (ld *boltLoadedDatabase) Properties(bucketPath []string) (string, error) {
content := fmt.Sprintf("Selected database: %#v", ld.db.Stats())
return content, nil
}
func (ld *boltLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load properties // Load properties
content := fmt.Sprintf("Selected database: %#v", ld.db.Stats())
f.propertiesBox.SetText(content)
if len(bucketPath) == 0 { if len(bucketPath) == 0 {
return nil // Can't have data outside of the top bucket return nil // Can't have data outside of the top bucket
} }
@@ -72,10 +75,7 @@ func (ld *boltLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string)
// Load data // Load data
// Bolt always uses Key + Value as the columns // Bolt always uses Key + Value as the columns
f.contentBox.SetColumnCount(2) f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"})
f.contentBox.SetHorizontalHeaderLabels([]string{"Key", "Value"})
f.contentBox.SetRowCount(0)
err := ld.db.View(func(tx *bbolt.Tx) error { err := ld.db.View(func(tx *bbolt.Tx) error {
b := boltTargetBucket(tx, bucketPath) b := boltTargetBucket(tx, bucketPath)
@@ -94,10 +94,7 @@ func (ld *boltLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string)
continue continue
} }
rpos := f.contentBox.RowCount() f.AddRow_PK_Data(k, k, v)
f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetItem(rpos, 0, qt.NewQTableWidgetItem2(formatUtf8(k)))
f.contentBox.SetItem(rpos, 1, qt.NewQTableWidgetItem2(formatUtf8(v)))
} }
return nil return nil
}) })
@@ -106,21 +103,70 @@ func (ld *boltLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string)
} }
// Valid // Valid
f.contentBox.ResizeColumnsToContents() f.Ready()
f.contentBox.SetEnabled(true)
return nil return nil
} }
func (n *boltLoadedDatabase) ApplyChanges(f *App, bucketPath []string) error { // kvstore_ApplyChanges is a helper function to apply edits to K/V stores that
if n.db.IsReadOnly() { // can use a common abstraction.
return errors.New("Database was opened read-only") // It always uses the "popupData" type i.e. []byte.
func kvstore_ApplyChanges(f *tableState, Put func(k, v []byte) error, Delete func(k []byte) error) error {
// Columns are two binColumn
keyCol := f.columns[0].(*binColumn)
valCol := f.columns[1].(*binColumn)
// Edit
for rowid, _ /*editcells*/ := range f.updateRows {
k_orig := f.primaryKeys[rowid]
k_new := keyCol.vals[rowid]
v := valCol.vals[rowid]
if !bytes.Equal(k_orig, k_new) {
// Editing the primary key
// Delete k_orig and only put in k_new
err := Delete(k_orig)
if err != nil {
return fmt.Errorf("Deleting cell %q: %w", formatUtf8(k_orig), err)
}
}
err := Put(k_new, []byte(v))
if err != nil {
return fmt.Errorf("Updating cell %q: %w", formatUtf8(k_new), err)
}
} }
// We have rendered row IDs, need to convert back to a bolt primary key // Delete by key (affects rowids after re-render)
// TODO stash the real key inside f.contentBox.Objects() for rowid, _ := range f.deleteRows {
// FIXME breaks if you try and edit the primary key(!) k := f.primaryKeys[rowid]
primaryKeyForRendered := func(rowid int) []byte { err := Delete(k)
return []byte(f.ui.contentBox.Item(rowid, 0).Text()) if err != nil {
return fmt.Errorf("Deleting cell %q: %w", formatUtf8(k), err)
}
}
// Insert all new entries
for rowid, _ := range f.insertRows {
// Newly inserted rows will not have a valid value stashed in the
// f.primaryKeys slice
// Have to get it from the data content of the cell directly
k := keyCol.vals[rowid]
v := valCol.vals[rowid] // There's only one value cell
err := Put(k, v)
if err != nil {
return fmt.Errorf("Inserting cell %q: %w", formatUtf8(k), err)
}
}
// Done
return nil
}
func (n *boltLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
if n.db.IsReadOnly() {
return errors.New("Database was opened read-only")
} }
return n.db.Update(func(tx *bbolt.Tx) error { return n.db.Update(func(tx *bbolt.Tx) error {
@@ -128,37 +174,7 @@ func (n *boltLoadedDatabase) ApplyChanges(f *App, bucketPath []string) error {
// Get current bucket handle // Get current bucket handle
b := boltTargetBucket(tx, bucketPath) b := boltTargetBucket(tx, bucketPath)
// Edit return kvstore_ApplyChanges(f, b.Put, b.Delete)
for rowid, _ /*editcells*/ := range f.updateRows {
k := primaryKeyForRendered(rowid)
v := f.ui.contentBox.Item(rowid, 1).Text() // There's only one value cell
err := b.Put(k, []byte(v))
if err != nil {
return fmt.Errorf("Updating cell %q: %w", formatUtf8(k), err)
}
}
// Delete by key (affects rowids after re-render)
for rowid, _ := range f.deleteRows {
k := primaryKeyForRendered(rowid)
err := b.Delete(k)
if err != nil {
return fmt.Errorf("Deleting cell %q: %w", formatUtf8(k), err)
}
}
// Insert all new entries
for rowid, _ := range f.insertRows {
k := primaryKeyForRendered(rowid)
v := f.ui.contentBox.Item(rowid, 1).Text() // There's only one value cell
err := b.Put(k, []byte(v))
if err != nil {
return fmt.Errorf("Inserting cell %q: %w", formatUtf8(k), err)
}
}
// Done
return nil
}) })
} }
@@ -167,14 +183,30 @@ func (ld *boltLoadedDatabase) NavChildren(bucketPath []string) ([]string, error)
return boltChildBucketNames(ld.db, bucketPath) return boltChildBucketNames(ld.db, bucketPath)
} }
func (ld *boltLoadedDatabase) NavContext(bucketPath []string) (ret []contextAction, err error) { func (ld *boltLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
ret = append(ret, contextAction{"Add bucket...", ld.AddChildBucket}) ret := []contextAction{
{"Add bucket...", ld.AddChildBucket},
}
if len(bucketPath) > 0 { if len(bucketPath) > 0 {
ret = append(ret, contextAction{"Delete bucket", ld.DeleteBucket}) ret = append(ret, contextAction{"Delete bucket", ld.DeleteBucket})
} }
return if len(bucketPath) == 0 {
ret = append(ret, contextAction{"Export to .zip...", ld.exportToZip})
}
return ret, nil
}
func (ld *boltLoadedDatabase) exportToZip(sender *qt.QTreeWidgetItem, bucketPath []string) error {
// Popup for output file
savePath := qt.QFileDialog_GetSaveFileName4(sender.TreeWidget().QWidget, "Save backup as...", "", "Zip archive (*.zip);;All files (*)")
if savePath == "" {
return nil // cancelled
}
return ld.exportDatabaseToZip(savePath)
} }
func (ld *boltLoadedDatabase) AddChildBucket(sender *qt.QTreeWidgetItem, bucketPath []string) error { func (ld *boltLoadedDatabase) AddChildBucket(sender *qt.QTreeWidgetItem, bucketPath []string) error {
@@ -201,6 +233,11 @@ func (ld *boltLoadedDatabase) AddChildBucket(sender *qt.QTreeWidgetItem, bucketP
} }
func (ld *boltLoadedDatabase) DeleteBucket(sender *qt.QTreeWidgetItem, bucketPath []string) error { func (ld *boltLoadedDatabase) DeleteBucket(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the bucket %q?", bucketPath[0])) {
return nil // cancelled
}
err := ld.db.Update(func(tx *bbolt.Tx) error { err := ld.db.Update(func(tx *bbolt.Tx) error {
// Find parent of this bucket. // Find parent of this bucket.
if len(bucketPath) >= 2 { if len(bucketPath) >= 2 {
@@ -227,6 +264,11 @@ var _ editableLoadedDatabase = &boltLoadedDatabase{} // interface assertion
// //
// boltTargetBucket resolves the bucketPath to a specific bolt.Bucket.
// If the path is empty, this function returns <nil> and you must manually access
// the root bucket.
// If the bucket does not exist, this function returns <nil>. It does not create
// the bucket.
func boltTargetBucket(tx *bbolt.Tx, path []string) *bbolt.Bucket { func boltTargetBucket(tx *bbolt.Tx, path []string) *bbolt.Bucket {
// If we are already deep in buckets, go directly there to find children // If we are already deep in buckets, go directly there to find children

230
db_bolt_zip.go Normal file
View File

@@ -0,0 +1,230 @@
package main
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"go.etcd.io/bbolt"
)
func (ld *boltLoadedDatabase) exportDatabaseToZip(zippath string) error {
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
// TODO undo this transfomation on import(!)
safename := func(n string) string {
return strings.ReplaceAll(string(n), `/`, `__`)
}
err = ld.db.View(func(tx *bbolt.Tx) error {
var process func(currentPath []string) error
process = func(currentPath []string) error {
// Create folder-entry for our own bucket
ourFolderName := path.Join(slice_apply(currentPath, safename)...)
ourBucket := zip.FileHeader{
Name: ourFolderName + `/`, // Trailing slash = directory
}
ourBucket.SetMode(fs.ModeDir | 0755)
_, err := zw.CreateHeader(&ourBucket)
if err != nil {
return err
}
// Create file entries for all non-bucket children
b := boltTargetBucket(tx, currentPath)
var childBuckets []string
var c *bbolt.Cursor
if b != nil {
c = b.Cursor() // in bucket
} else {
c = tx.Cursor()
}
for k, v := c.First(); k != nil; k, v = c.Next() {
if v == nil {
// That's a bucket
childBuckets = append(childBuckets, string(k))
continue
}
fileItem := zip.FileHeader{
Name: path.Join(ourFolderName, safename(string(k))),
}
fileItem.SetMode(0644)
fileW, err := zw.CreateHeader(&fileItem)
if err != nil {
return err
}
_, err = io.CopyN(fileW, bytes.NewReader(v), int64(len(v)))
if err != nil {
return err
}
}
// Recurse for all bucket-type children
for _, childBucketName := range childBuckets {
process(slice_and(currentPath, childBucketName))
}
// Done
return nil
}
return 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 (f *App) Bolt_ImportZipToDatabase_OnTriggered() {
zippath := qt.QFileDialog_GetOpenFileName4(f.ui.MainWindow.QWidget, "Select a zip archive to import...", "", "Zip archives (*.zip);;All files (*)")
if zippath == "" {
return // cancelled
}
dbpath := qt.QFileDialog_GetSaveFileName4(f.ui.MainWindow.QWidget, "Select an output file to save as...", "", "Bolt database (*.db);;All files (*)")
if dbpath == "" {
return // cancelled
}
err := Bolt_ImportZipToDatabase(dbpath, zippath)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
return
}
res := qt.QMessageBox_Question2(f.ui.MainWindow.QWidget, APPNAME, "The import was successful. Would you like to open the Bolt database now?", qt.QMessageBox__Yes, qt.QMessageBox__No)
if res != int(qt.QMessageBox__Yes) {
return
}
config := NewConnectionConfig()
config.Type = "Bolt"
config.Bolt = &boltConfig{
Path: autoconfig.ExistingFile(dbpath),
Readonly: false,
}
f.showConnectDialog(config)
}
func Bolt_ImportZipToDatabase(dbpath, zippath string) error {
db, err := bbolt.Open(dbpath, 0644, nil)
if err != nil {
return fmt.Errorf("Opening target database: %w", err)
}
defer db.Close()
fh, err := os.OpenFile(zippath, os.O_RDONLY, 0400)
if err != nil {
return fmt.Errorf("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)
}
return db.Update(func(tx *bbolt.Tx) error {
for _, zf := range zr.File {
if strings.HasSuffix(zf.Name, `/`) || (zf.Mode()&fs.ModeDir) != 0 {
// Bucket
if zf.Name == `/` {
continue // virtual entry for top-level directory, skip
}
bucketPath := strings.Split(strings.TrimSuffix(zf.Name, `/`), `/`)
parentBucket := boltTargetBucket(tx, bucketPath[0:len(bucketPath)-1])
newBucketName := []byte(bucketPath[len(bucketPath)-1])
var err error
if parentBucket == nil {
_, err = tx.CreateBucket(newBucketName) // at top level
} else {
_, err = parentBucket.CreateBucket(newBucketName) // child bucket
}
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
}
parentBucket := boltTargetBucket(tx, objectPath[0:len(objectPath)-1]) // Can't be nil, items always exist within a bucket
objectKey := []byte(objectPath[len(objectPath)-1])
err = parentBucket.Put(objectKey, content)
if err != nil {
return err
}
err = rc.Close()
if err != nil {
return err
}
}
}
// Done
return nil
})
}

118
db_bunt.go Normal file
View File

@@ -0,0 +1,118 @@
package main
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"github.com/tidwall/buntdb"
)
type buntLdb struct {
db *buntdb.DB
}
func (ld *buntLdb) DriverName() string {
return "BuntDB"
}
func (ld *buntLdb) Properties(bucketPath []string) (string, error) {
idxInfo, err := ld.db.Indexes()
if err != nil {
return "", err
}
return fmt.Sprintf("Indexes (%d):\n%s", len(idxInfo), "-"+strings.Join(idxInfo, "\n- ")+"\n"), nil
}
func (ld *buntLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
// Interestingly, BuntDB internally uses string data, not []byte data
// That would stop us from using kvstore_ApplyChanges that only works with
// []byte data, so, fake []byte casts ourselves
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"})
err := ld.db.View(func(tx *buntdb.Tx) error {
return tx.Ascend("", func(k, v string) bool {
f.AddRow_PK_Data([]byte(k), []byte(k), []byte(v))
return true
})
})
if err != nil {
return err
}
// Valid
f.Ready()
return nil
}
func (n *buntLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return n.db.Update(func(tx *buntdb.Tx) error {
return kvstore_ApplyChanges(
f,
func(k, v []byte) error {
_, _, err := tx.Set(string(k), string(v), nil)
return err
},
func(k []byte) error {
_, err := tx.Delete(string(k))
return err
},
)
})
}
func (ld *buntLdb) doShrink(sender *qt.QTreeWidgetItem, bucketPath []string) error {
return ld.db.Shrink()
}
func (ld *buntLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *buntLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{"Shrink", ld.doShrink},
}, nil
}
func (ld *buntLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &buntLdb{} // interface assertion
var _ editableLoadedDatabase = &buntLdb{} // interface assertion
//
type buntDBConnection struct {
Type autoconfig.OneOf
Disk *struct {
File autoconfig.ExistingFile
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
}
func (ldc *buntDBConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
var path string
if ldc.Disk != nil {
path = string(ldc.Disk.File)
} else {
path = `:memory:` // Special string known by driver
}
db, err := buntdb.Open(path)
if err != nil {
return nil, "", err
}
return &buntLdb{db: db}, filepath.Base(path), nil
}

View File

@@ -5,11 +5,11 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"yvbolt/debconf" "qbolt/debconf"
"github.com/mappu/autoconfig" "github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
) )
type debconfLoadedDatabase struct { type debconfLoadedDatabase struct {
@@ -20,43 +20,62 @@ func (ld *debconfLoadedDatabase) DriverName() string {
return "debconf" return "debconf"
} }
func (ld *debconfLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string) error { func (ld *debconfLoadedDatabase) Properties(bucketPath []string) (string, error) {
content := fmt.Sprintf("Applications: %d\nUnique attributes: %d\n", len(ld.db.Entries), len(ld.db.AllColumnNames))
return content, nil
}
// Load properties func (ld *debconfLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
content := fmt.Sprintf("Entries: %d\nUnique attributes: %d\n", len(ld.db.Entries), len(ld.db.AllColumnNames))
f.propertiesBox.SetText(content)
// Load data // Load data
if len(bucketPath) == 0 {
return nil // No data at root level
}
// Find application entry for bucketPath
appInfo, ok := ld.db.FindApplicationByName(bucketPath[0])
if !ok {
return fmt.Errorf("Invalid application %q", bucketPath[0])
}
// Process entries for specific application
indexes := make(map[string]int) indexes := make(map[string]int)
f.contentBox.SetColumnCount(len(ld.db.AllColumnNames)) f.SetupColumns(slice_repeat(columnType_inlineText, len(ld.db.AllColumnNames)), ld.db.AllColumnNames)
f.contentBox.SetHorizontalHeaderLabels(ld.db.AllColumnNames)
for i, cname := range ld.db.AllColumnNames { for i, cname := range ld.db.AllColumnNames {
indexes[cname] = i indexes[cname] = i
} }
for _, entry := range ld.db.Entries { for _, entry := range appInfo.Entries {
rpos := f.contentBox.RowCount() rpos := f.AddRow()
f.contentBox.SetRowCount(rpos + 1) f.SetCell(rpos, 0, entry.Name)
f.contentBox.SetItem(rpos, 0, qt.NewQTableWidgetItem2(entry.Name))
for _, proppair := range entry.Properties { for _, proppair := range entry.Properties {
f.contentBox.SetItem(rpos, indexes[proppair[0]], qt.NewQTableWidgetItem2(proppair[1])) f.SetCell(rpos, indexes[proppair[0]], proppair[1])
} }
} }
// Valid // Valid
f.contentBox.ResizeColumnsToContents() f.Ready()
f.contentBox.SetEnabled(true)
return nil return nil
} }
func (ld *debconfLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) { func (ld *debconfLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children if len(bucketPath) == 0 {
ret := make([]string, 0, len(ld.db.Entries))
for _, app := range ld.db.Entries {
ret = append(ret, app.Name)
}
return ret, nil
} else {
return []string{}, nil // No further children
}
} }
func (ld *debconfLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) { func (ld *debconfLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
@@ -73,7 +92,13 @@ var _ loadedDatabase = &debconfLoadedDatabase{} // interface assertion
// //
type debconfConnection struct { type debconfConnection struct {
Database autoconfig.ExistingFile `yfilter:"Debconf database (*.dat);;All files (*.*)"` Database autoconfig.ExistingFile `yfilter:"Debconf database (*.dat);;All files (*)"`
}
func (dc *debconfConnection) Reset() {
if runtime.GOOS == "linux" {
dc.Database = "/var/cache/debconf/config.dat" // Prefill default path
}
} }
func (dc *debconfConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { func (dc *debconfConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
@@ -90,9 +115,3 @@ func (dc *debconfConnection) Connect(ctx context.Context) (loadedDatabase, strin
return &debconfLoadedDatabase{db: db}, filepath.Base(string(dc.Database)), nil return &debconfLoadedDatabase{db: db}, filepath.Base(string(dc.Database)), nil
} }
func init() {
registerDatabase(&debconfConnection{
Database: "/var/cache/debconf/config.dat", // Prefill default path
}, "Debconf", `:/assets/vendor_debian.png`)
}

61
db_embeddedversions.go Normal file
View File

@@ -0,0 +1,61 @@
package main
import (
"context"
"errors"
"fmt"
"runtime/debug"
)
type evLdb struct {
mods *debug.BuildInfo
}
func (ld *evLdb) DriverName() string {
return APPNAME + " " + appVersion
}
func (ld *evLdb) Properties(bucketPath []string) (string, error) {
return fmt.Sprintf(
"%s %s\n- %d package dependencies\n- Compiler: %s",
APPNAME, appVersion, len(ld.mods.Deps), ld.mods.GoVersion,
), nil
}
func (ld *evLdb) RenderForNav(f *tableState, bucketPath []string) error {
f.SetupColumns([]columnType{columnType_inlineText, columnType_inlineText, columnType_inlineText}, []string{"Library", "Version", "Hash"})
for _, dep := range ld.mods.Deps {
f.AddRowData(dep.Path, dep.Version, dep.Sum)
}
// Valid
f.Ready()
return nil
}
func (ld *evLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No further children
}
func (ld *evLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *evLdb) Close() {}
var _ loadedDatabase = &evLdb{} // interface assertion
//
type evLdbConnection struct{}
func (dc *evLdbConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
mods, ok := debug.ReadBuildInfo()
if !ok {
return nil, "", errors.New("Missing build info")
}
return &evLdb{mods: mods}, APPNAME, nil
}

View File

@@ -6,7 +6,6 @@ import (
"path/filepath" "path/filepath"
"github.com/mappu/autoconfig" "github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt" "github.com/syndtr/goleveldb/leveldb/opt"
) )
@@ -19,43 +18,55 @@ func (ld *leveldbLoadedDatabase) DriverName() string {
return "LevelDB" return "LevelDB"
} }
func (ld *leveldbLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string) error { func (ld *leveldbLoadedDatabase) Properties(bucketPath []string) (string, error) {
// Load properties
var s leveldb.DBStats var s leveldb.DBStats
err := ld.db.Stats(&s) err := ld.db.Stats(&s)
if err != nil { if err != nil {
return fmt.Errorf("Stats: %w", err) return "", fmt.Errorf("Stats: %w", err)
} }
content := fmt.Sprintf("LevelDB stats: %#v", s) content := fmt.Sprintf("LevelDB stats: %#v", s)
f.propertiesBox.SetText(content) return content, nil
}
func (ld *leveldbLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load data // Load data
// leveldb always uses Key + Value as the columns // leveldb always uses Key + Value as the columns
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"})
f.contentBox.SetColumnCount(2)
f.contentBox.SetHorizontalHeaderLabels([]string{"Key", "Value"})
itn := ld.db.NewIterator(nil, nil) itn := ld.db.NewIterator(nil, nil)
defer itn.Release() defer itn.Release()
for itn.Next() { for itn.Next() {
f.AddRow_PK_Data(itn.Key(), itn.Key(), itn.Value())
rpos := f.contentBox.RowCount()
f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetItem(rpos, 0, qt.NewQTableWidgetItem2(string(itn.Key())))
f.contentBox.SetItem(rpos, 1, qt.NewQTableWidgetItem2(string(itn.Value())))
} }
// Valid // Valid
f.contentBox.ResizeColumnsToContents() f.Ready()
f.contentBox.SetEnabled(true)
return nil return nil
} }
func (n *leveldbLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
txn, err := n.db.OpenTransaction()
if err != nil {
return fmt.Errorf("OpenTransaction: %w", err)
}
err = kvstore_ApplyChanges(
f,
func(k, v []byte) error { return txn.Put(k, v, nil) },
func(k []byte) error { return txn.Delete(k, nil) },
)
if err != nil {
txn.Discard()
return err
}
return txn.Commit()
}
func (ld *leveldbLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) { func (ld *leveldbLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children return []string{}, nil // No children
} }
@@ -68,16 +79,17 @@ func (ld *leveldbLoadedDatabase) Close() {
_ = ld.db.Close() _ = ld.db.Close()
} }
var _ loadedDatabase = &leveldbLoadedDatabase{} // interface assertion var _ loadedDatabase = &leveldbLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &leveldbLoadedDatabase{} // interface assertion
// //
type leveldbDirConnection struct { type leveldbConnection struct {
Directory autoconfig.ExistingDirectory Directory autoconfig.ExistingDirectory
Readonly bool Readonly bool
} }
func (pdc *leveldbDirConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { func (pdc *leveldbConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
var o opt.Options var o opt.Options
o.ReadOnly = pdc.Readonly o.ReadOnly = pdc.Readonly
@@ -88,7 +100,3 @@ func (pdc *leveldbDirConnection) Connect(ctx context.Context) (loadedDatabase, s
return &leveldbLoadedDatabase{db: db}, filepath.Base(string(pdc.Directory)), nil return &leveldbLoadedDatabase{db: db}, filepath.Base(string(pdc.Directory)), nil
} }
func init() {
registerDatabase(&leveldbDirConnection{}, "LevelDB", `:/assets/vendor_leveldb.png`)
}

284
db_lmdb.go Normal file
View File

@@ -0,0 +1,284 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/ledgerwatch/lmdb-go/lmdb"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
)
type lmdbDatabase struct {
db *lmdb.Env
isMulti bool
}
func (ld *lmdbDatabase) DriverName() string {
return lmdb.VersionString() // Already includes "LMDB" prefix
}
func (ld *lmdbDatabase) Properties(bucketPath []string) (string, error) {
info, err := ld.db.Info()
if err != nil {
return "", err
}
return fmt.Sprintf("LMDB info: %#v", info), nil
}
func (ld *lmdbDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
if ld.isMulti && len(bucketPath) == 0 {
// In multi-database mode, the only things in the root DB are keys
// naming the child databases
// Show no data, it will be shown in the nav area instead
return nil
}
// LMDB always uses Key + Value as the columns
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"})
err := ld.db.View(func(txn *lmdb.Txn) error {
var dbi lmdb.DBI
var err error
if len(bucketPath) == 0 {
dbi, err = txn.OpenRoot(0)
} else {
dbi, err = txn.OpenDBI(bucketPath[0], 0)
}
if err != nil {
return err
}
// defer ld.db.CloseDBI(dbi)
itn, err := txn.OpenCursor(dbi)
defer itn.Close()
if err != nil {
return err
}
// FIXME is this correct?
kbuff := make([]byte, ld.db.MaxKeySize())
vbuff := make([]byte, 4096)
var op uint = lmdb.First
for {
key, val, err := itn.Get(kbuff, vbuff, op)
if err != nil {
if lmdb.IsNotFound(err) {
break // reached end of iteration
}
return err // a real error
}
op = lmdb.Next
f.AddRow_PK_Data(key, key, val)
}
// Done
return nil
})
if err != nil {
return err
}
// Valid
f.Ready()
return nil
}
func (ld *lmdbDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
return ld.db.Update(func(txn *lmdb.Txn) error {
var dbi lmdb.DBI
var err error
if len(bucketPath) == 0 {
dbi, err = txn.OpenRoot(0)
} else {
dbi, err = txn.OpenDBI(bucketPath[0], 0)
}
if err != nil {
return err
}
return kvstore_ApplyChanges(
f,
func(k, v []byte) error { return txn.Put(dbi, k, v, 0) },
func(k []byte) error { return txn.Del(dbi, k, nil) },
)
})
}
func (ld *lmdbDatabase) NavChildren(bucketPath []string) ([]string, error) {
// """To use named databases (with name != NULL), mdb_env_set_maxdbs() must
// be called before opening the environment. Database names are keys in the
// unnamed database, and may be read but not written."""
if len(bucketPath) == 0 && ld.isMulti {
// Read all keys from root DB
var allKeys []string
err := ld.db.View(func(txn *lmdb.Txn) error {
dbi, err := txn.OpenRoot(0)
if err != nil {
return err
}
//defer ld.db.CloseDBI(dbi)
itn, err := txn.OpenCursor(dbi)
defer itn.Close()
if err != nil {
return err
}
kbuff := make([]byte, ld.db.MaxKeySize())
var op uint = lmdb.First
for {
key, _, err := itn.Get(kbuff, nil, op)
if err != nil {
if lmdb.IsNotFound(err) {
break // reached end of iteration
}
return err // a real error
}
op = lmdb.Next
allKeys = append(allKeys, string(key))
}
// Done
return nil
})
return allKeys, err
} else {
return []string{}, nil // No children
}
}
func (ld *lmdbDatabase) createChildDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
// Prompt for child database name
ok := false
childDbName := qt.QInputDialog_GetText4(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the child database:", qt.QLineEdit__Normal, "", &ok)
if !ok {
return nil // Cancelled
}
return ld.db.Update(func(txn *lmdb.Txn) error {
_, err := txn.CreateDBI(childDbName)
return err
})
}
func (ld *lmdbDatabase) truncateAllContent(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to empty the child database %q?", bucketPath[0])) {
return nil // cancelled
}
return ld.drop(bucketPath[0], true)
}
func (ld *lmdbDatabase) deleteChildDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the child database %q?", bucketPath[0])) {
return nil // cancelled
}
return ld.drop(bucketPath[0], true)
}
func (ld *lmdbDatabase) drop(multiDbName string, delet bool) error {
return ld.db.Update(func(txn *lmdb.Txn) error {
dbi, err := txn.OpenDBI(multiDbName, 0)
if err != nil {
return err
}
return txn.Drop(dbi, true)
})
}
func (ld *lmdbDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
if ld.isMulti {
// Allow create/delete databases
if len(bucketPath) == 0 {
return []contextAction{
contextAction{"Add child database...", ld.createChildDatabase},
}, nil
} else {
return []contextAction{
contextAction{"Truncate and remove all contents...", ld.truncateAllContent},
contextAction{"Remove child database...", ld.deleteChildDatabase},
}, nil
}
}
return nil, nil // No special actions are supported
}
func (ld *lmdbDatabase) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &lmdbDatabase{} // interface assertion
var _ editableLoadedDatabase = &lmdbDatabase{} // interface assertion
//
type lmdbConnection struct {
Storage struct {
Type autoconfig.OneOf
Directory *autoconfig.ExistingDirectory `json:",omitempty"`
File *struct {
Path autoconfig.ExistingFile `yfilter:"LMDB database (*.mdb);;All files (*)"`
} `json:",omitempty"`
}
MultiDB *struct {
Slots int
} `ylabel:"Multiple databases mode"`
Readonly bool
}
func (pdc *lmdbConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
env, err := lmdb.NewEnv()
if err != nil {
return nil, "", err
}
var openPath string
var flags uint = 0
if pdc.Readonly {
flags |= lmdb.Readonly
}
if pdc.Storage.Directory != nil {
openPath = string(*pdc.Storage.Directory)
} else if pdc.Storage.File != nil {
openPath = string(pdc.Storage.File.Path)
flags |= lmdb.NoSubdir
}
isMulti := false
if pdc.MultiDB != nil {
env.SetMaxDBs(pdc.MultiDB.Slots)
isMulti = true
}
err = env.Open(openPath, flags, 0644)
if err != nil {
_ = env.Close()
return nil, "", err
}
return &lmdbDatabase{db: env, isMulti: isMulti}, filepath.Base(openPath), nil
}

88
db_lotusdb.go Normal file
View File

@@ -0,0 +1,88 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/lotusdblabs/lotusdb/v2"
"github.com/mappu/autoconfig"
)
type lotusLdb struct {
db *lotusdb.DB
}
func (ld *lotusLdb) DriverName() string {
return "LotusDB"
}
func (ld *lotusLdb) Properties(bucketPath []string) (string, error) {
return "", nil
}
func (ld *lotusLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"})
cur, err := ld.db.NewIterator(lotusdb.IteratorOptions{})
if err != nil {
return fmt.Errorf("NewIterator: %w", err)
}
defer cur.Close()
for cur.Valid() {
f.AddRow_PK_Data(cur.Key(), cur.Key(), cur.Value())
cur.Next()
}
// Valid
f.Ready()
return nil
}
func (n *lotusLdb) ApplyChanges(f *tableState, bucketPath []string) error {
txn := n.db.NewBatch(lotusdb.DefaultBatchOptions)
err := kvstore_ApplyChanges(f, txn.Put, txn.Delete)
if err != nil {
return err
}
return txn.Commit()
}
func (ld *lotusLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *lotusLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *lotusLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &lotusLdb{} // interface assertion
var _ editableLoadedDatabase = &lotusLdb{} // interface assertion
//
type lotusDBConnection struct {
Directory autoconfig.ExistingDirectory
}
func (ldc *lotusDBConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
opts := lotusdb.DefaultOptions // copy
opts.DirPath = string(ldc.Directory)
db, err := lotusdb.Open(opts)
if err != nil {
return nil, "", err
}
return &lotusLdb{db: db}, filepath.Base(string(ldc.Directory)), nil
}

348
db_mongo.go Normal file
View File

@@ -0,0 +1,348 @@
package main
import (
"context"
"errors"
"fmt"
"time"
"golang.org/x/crypto/ssh"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"go.mongodb.org/mongo-driver/v2/mongo/readpref"
"go.mongodb.org/mongo-driver/v2/version"
)
// MongoDB support
// To test: `make test-mongo`
type mongoLdb struct {
client *mongo.Client
sshc *ssh.Client
}
func (ld *mongoLdb) DriverName() string {
return "MongoDB " + version.Driver
}
func (ld *mongoLdb) Properties(bucketPath []string) (string, error) {
ctx := context.Background()
if len(bucketPath) == 0 {
return "", nil // no properties
} else if len(bucketPath) == 1 {
// Database is selected
db := ld.client.Database(bucketPath[0])
return "Database " + db.Name(), nil
} else if len(bucketPath) == 2 {
// Collection is selected
db := ld.client.Database(bucketPath[0])
coll := db.Collection(bucketPath[1])
info := "Database " + db.Name() + "\n"
info += "Collection " + coll.Name() + "\n"
// Document count
count, err := coll.EstimatedDocumentCount(ctx)
if err != nil {
return "", fmt.Errorf("Estimating document count: %w", err)
}
info += fmt.Sprintf("Estimated document count: %d", count) + "\n"
// Index info
allIndexes, err := coll.Indexes().ListSpecifications(ctx)
if err != nil {
return "", fmt.Errorf("Checking indexes: %w", err)
}
info += fmt.Sprintf("\nIndexes (%d):\n", len(allIndexes))
for _, idxInfo := range allIndexes {
info += fmt.Sprintf("- %q (namespace=%q, version=%d)\n", idxInfo.Name, idxInfo.Namespace, idxInfo.Version)
}
return info, nil
} else {
return "", errors.New("??")
}
}
func (ld *mongoLdb) RenderForNav(f *tableState, bucketPath []string) error {
ctx := context.Background()
if len(bucketPath) == 0 || len(bucketPath) == 1 {
// Leave the table disabled
return nil
} else if len(bucketPath) == 2 {
f.SetupColumns([]columnType{columnType_inlineText, columnType_bsonDoc}, []string{"_id", "Document"})
db := ld.client.Database(bucketPath[0])
coll := db.Collection(bucketPath[1])
cur, err := coll.Find(ctx, bson.D{}) // An empty document as filter = find all results
if err != nil {
return fmt.Errorf("Find: %w", err)
}
defer cur.Close(ctx)
return ld.populateRows(ctx, cur, f)
} else {
return errors.New("??")
}
}
func (ld *mongoLdb) populateRows(ctx context.Context, cur *mongo.Cursor, f *tableState) error {
for cur.Next(ctx) {
var result bson.D
if err := cur.Decode(&result); err != nil {
return fmt.Errorf("Decode: %w", err)
}
// The document is always an ordered map[string]any.
// MongoDB enforces there is an "_id" key.
idValue, ok := bson_find_id(result)
if !ok {
return errors.New("Surprised to find a document missing an '_id' field")
}
f.AddRow_PK_Data([]byte(idValue), idValue, result)
}
if err := cur.Err(); err != nil {
return fmt.Errorf("Cursor: %w", err)
}
// Done
f.Ready()
return nil
}
func (ld *mongoLdb) NavChildren(bucketPath []string) ([]string, error) {
ctx := context.Background()
if len(bucketPath) == 0 {
return ld.client.ListDatabaseNames(ctx, bson.D{})
} else if len(bucketPath) == 1 {
db := ld.client.Database(bucketPath[0])
return db.ListCollectionNames(ctx, bson.D{})
}
return []string{}, nil // No children
}
func (ld *mongoLdb) actionNewDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
dbName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new database:")
if dbName == "" {
return nil // cancel
}
// MongoDB: databases just start to exist when you write to them
newDb := ld.client.Database(dbName)
return newDb.CreateCollection(ctx, "my.new.collection")
}
func (ld *mongoLdb) actionNewCollection(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
collName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new collection:")
if collName == "" {
return nil // cancel
}
// MongoDB: databases just start to exist when you write to them
newDb := ld.client.Database(bucketPath[0])
return newDb.CreateCollection(ctx, collName)
}
func (ld *mongoLdb) actionDropDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to drop the database %q?", bucketPath[0])) {
return nil // cancelled
}
db := ld.client.Database(bucketPath[0])
return db.Drop(ctx)
}
func (ld *mongoLdb) actionDropCollection(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the collection %q?", bucketPath[1])) {
return nil // cancelled
}
db := ld.client.Database(bucketPath[0])
coll := db.Collection(bucketPath[1])
return coll.Drop(ctx)
}
func (ld *mongoLdb) ExecQuery(query string, bucketPath []string, resultArea *tableState) error {
ctx := context.Background()
if len(bucketPath) == 0 {
return errors.New("Please select a database first.")
}
db := ld.client.Database(bucketPath[0])
// The query should be JSON, e.g.
// { "hello": 1 } or
// { "explain": { "count": "system.users" } } or
// { "listDatabases": 1 }
doc := bson.D{}
err := doc.UnmarshalJSON([]byte(query))
if err != nil {
return fmt.Errorf("Parsing JSON query: %w", err)
}
// FIXME how to tell if the response will have a cursor or a singleResult?
if false {
// Cursor
cur, err := db.RunCommandCursor(ctx, doc)
if err != nil {
return fmt.Errorf("Running command: %w", err)
}
defer cur.Close(ctx)
return ld.populateRows(ctx, cur, resultArea)
} else {
// Single result
res := db.RunCommand(ctx, doc)
response := bson.D{}
err = res.Decode(&response)
if err != nil {
return fmt.Errorf("Decoding response: %w", err)
}
responseJson, err := response.MarshalJSON()
if err != nil {
return fmt.Errorf("Decoding response: %w", err)
}
resultArea.SetupColumns([]columnType{columnType_popupData}, []string{"Response"})
resultArea.AddRowData(responseJson)
resultArea.Ready()
return nil
}
}
func (ld *mongoLdb) NavContext(bucketPath []string) ([]contextAction, error) {
if len(bucketPath) == 0 {
// Top-level connection
return []contextAction{
{"Create database...", ld.actionNewDatabase},
}, nil
} else if len(bucketPath) == 1 {
// Database selected
return []contextAction{
{"Create collection...", ld.actionNewCollection},
{"Drop database...", ld.actionDropDatabase},
}, nil
} else if len(bucketPath) == 2 {
// Collection selected
return []contextAction{
{"Drop collection...", ld.actionDropCollection},
}, nil
} else {
return nil, errors.New("???")
}
}
func (ld *mongoLdb) Close() {
_ = ld.client.Disconnect(context.Background())
if ld.sshc != nil {
_ = ld.sshc.Close()
}
}
var _ loadedDatabase = &mongoLdb{} // interface assertion
var _ queryableLoadedDatabase = &mongoLdb{} // interface assertion
//
type mongoConnection struct {
Conn struct {
Mode autoconfig.OneOf
Connection_String *string `json:",omitempty"`
}
SSH_Tunnel *SSHTunnel
}
func (moc *mongoConnection) Reset() {
moc.Conn.Mode = "Connection_String"
moc.Conn.Connection_String = address_of("mongodb://localhost:27017")
}
func (moc *mongoConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
ctx, _ = context.WithTimeout(ctx, 5*time.Second) // shadow parent ctx
opts := options.Client().ApplyURI(string(*moc.Conn.Connection_String))
// Our used library supports all compressors
opts.SetCompressors([]string{"zstd", "snappy", "zlib"})
ret := mongoLdb{}
if moc.SSH_Tunnel != nil {
sshc, err := moc.SSH_Tunnel.Open(ctx)
if err != nil {
return nil, "", err
}
opts.Dialer = sshc // interface implements DialContext()
ret.sshc = sshc
// The crypto/ssh library does not support deadlines over tcp tunnels
// Go-redis has a workaround for this, but go-mongo does not
}
//
client, err := mongo.Connect(opts)
if err != nil {
return nil, "", err
}
ret.client = client
// Connect() does not block for server discovery. Check that the server really
// is reachable
err = client.Ping(ctx, readpref.Primary())
if err != nil {
return nil, "", err
}
// We should be able to ListDatabases - there may be an authentication error
_, err = client.ListDatabaseNames(ctx, bson.D{})
if err != nil {
return nil, "", err
}
return &ret, "MongoDB", nil
}

View File

@@ -6,8 +6,10 @@ func (n *noLoadedDatabase) DriverName() string {
return "No database selected" return "No database selected"
} }
func (n *noLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string) error { func (n *noLoadedDatabase) Properties(bucketPath []string) (string, error) {
f.propertiesBox.SetText("Open a database to get started...") return "Open a database to get started...", nil
}
func (n *noLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
return nil return nil
} }

View File

@@ -4,61 +4,33 @@ import (
"context" "context"
"fmt" "fmt"
"path/filepath" "path/filepath"
"runtime/debug"
"sync"
"github.com/cockroachdb/pebble" "github.com/cockroachdb/pebble"
"github.com/cockroachdb/pebble/vfs" "github.com/cockroachdb/pebble/vfs"
"github.com/mappu/autoconfig" "github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
) )
type pebbleLoadedDatabase struct { type pebbleLoadedDatabase struct {
db *pebble.DB db *pebble.DB
} }
var (
version_Pebble_once sync.Once
version_Pebble string
)
func pebbleDriverVersion() string {
version_Pebble_once.Do(func() {
// ReadBuildInfo still works even with `-ldflags '-s -w'`.
mm, ok := debug.ReadBuildInfo()
if !ok {
panic("Missing build info")
}
for _, dep := range mm.Deps {
switch dep.Path {
case "github.com/cockroachdb/pebble":
version_Pebble = dep.Version
}
}
})
return version_Pebble
}
func (ld *pebbleLoadedDatabase) DriverName() string { func (ld *pebbleLoadedDatabase) DriverName() string {
return "Pebble " + pebbleDriverVersion() // Parsed from embedded go.mod copy return "Pebble"
} }
func (ld *pebbleLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string) error { func (ld *pebbleLoadedDatabase) Properties(bucketPath []string) (string, error) {
content := fmt.Sprintf("Pebble metrics: %#v", ld.db.Metrics())
return content, nil
}
func (ld *pebbleLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
ctx := context.Background() ctx := context.Background()
// Load properties
content := fmt.Sprintf("Pebble metrics: %#v", ld.db.Metrics())
f.propertiesBox.SetText(content)
// Load data // Load data
// pebble always uses Key + Value as the columns // pebble always uses Key + Value as the columns
f.contentBox.SetColumnCount(2) f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"})
f.contentBox.SetHorizontalHeaderLabels([]string{"Key", "Value"})
itr, err := ld.db.NewIterWithContext(ctx, nil) itr, err := ld.db.NewIterWithContext(ctx, nil)
if err != nil { if err != nil {
@@ -73,19 +45,29 @@ func (ld *pebbleLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []strin
return fmt.Errorf("Failed to load data for key %q: %w", formatAny(k), err) return fmt.Errorf("Failed to load data for key %q: %w", formatAny(k), err)
} }
rpos := f.contentBox.RowCount() f.AddRow_PK_Data(k, k, v)
f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetItem(rpos, 0, qt.NewQTableWidgetItem2(formatUtf8(k)))
f.contentBox.SetItem(rpos, 1, qt.NewQTableWidgetItem2(formatUtf8(v)))
} }
// Valid // Valid
f.contentBox.ResizeColumnsToContents() f.Ready()
f.contentBox.SetEnabled(true)
return nil return nil
} }
func (n *pebbleLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
txn := n.db.NewBatch()
err := kvstore_ApplyChanges(
f,
func(k, v []byte) error { return txn.Set(k, v, nil) },
func(k []byte) error { return txn.Delete(k, nil) },
)
if err != nil {
return err
}
return txn.Commit(nil)
}
func (ld *pebbleLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) { func (ld *pebbleLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children return []string{}, nil // No children
} }
@@ -98,41 +80,41 @@ func (ld *pebbleLoadedDatabase) Close() {
_ = ld.db.Close() _ = ld.db.Close()
} }
var _ loadedDatabase = &pebbleLoadedDatabase{} // interface assertion var _ loadedDatabase = &pebbleLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &pebbleLoadedDatabase{} // interface assertion
// //
type pebbleMemConnection struct{} type pebbleConnection struct {
Type autoconfig.OneOf
func (pmc *pebbleMemConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { Disk *struct {
db, err := pebble.Open("", &pebble.Options{FS: vfs.NewMem()}) Directory autoconfig.ExistingDirectory
if err != nil { Readonly bool
return nil, "", err } `json:",omitempty"`
} Memory *struct{} `json:",omitempty"`
return &pebbleLoadedDatabase{db: db}, `:memory:`, nil // SQLite-style naming
} }
// func (pdc *pebbleConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
type pebbleDirConnection struct {
Directory autoconfig.ExistingDirectory
Readonly bool
}
func (pdc *pebbleDirConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
opts := (&pebble.Options{}).EnsureDefaults() opts := (&pebble.Options{}).EnsureDefaults()
opts.ReadOnly = pdc.Readonly
db, err := pebble.Open(string(pdc.Directory), opts) if pdc.Disk != nil {
if err != nil { opts.ReadOnly = pdc.Disk.Readonly
return nil, "", err
db, err := pebble.Open(string(pdc.Disk.Directory), opts)
if err != nil {
return nil, "", err
}
return &pebbleLoadedDatabase{db: db}, filepath.Base(string(pdc.Disk.Directory)), nil
} else {
// Memory != nil
db, err := pebble.Open("", &pebble.Options{FS: vfs.NewMem()})
if err != nil {
return nil, "", err
}
return &pebbleLoadedDatabase{db: db}, `:memory:`, nil // SQLite-style naming
} }
return &pebbleLoadedDatabase{db: db}, filepath.Base(string(pdc.Directory)), nil
}
func init() {
registerDatabase(&pebbleMemConnection{}, "Pebble (Memory)", `:/assets/vendor_cockroach.png`)
registerDatabase(&pebbleDirConnection{}, "Pebble", `:/assets/vendor_cockroach.png`)
} }

View File

@@ -5,10 +5,9 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"yvbolt/lexer" "qbolt/lexer"
"github.com/mappu/autoconfig" "github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
@@ -20,10 +19,8 @@ type redisConnectionOptions struct {
SSH_Tunnel *SSHTunnel SSH_Tunnel *SSHTunnel
} }
func init() { func (config *redisConnectionOptions) Reset() {
registerDatabase(&redisConnectionOptions{ config.Address.Port = 6379
Address: autoconfig.AddressPort{Port: 6379}, // default value
}, "Redis", ":/assets/vendor_redis.png")
} }
func (config *redisConnectionOptions) Connect(ctx context.Context) (loadedDatabase, string, error) { func (config *redisConnectionOptions) Connect(ctx context.Context) (loadedDatabase, string, error) {
@@ -105,18 +102,27 @@ func (ld *redisLoadedDatabase) DriverName() string {
return "Redis " + ld.serverVersion return "Redis " + ld.serverVersion
} }
func (ld *redisLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string) error { func (ld *redisLoadedDatabase) Properties(bucketPath []string) (string, error) {
ctx := context.Background() ctx := context.Background()
if len(bucketPath) == 0 { if len(bucketPath) == 0 {
// Top-level: Show info() on main Properties tab // Top-level: Show info() on main Properties tab
infostr, err := ld.db.Info(ctx).Result() infostr, err := ld.db.Info(ctx).Result()
if err != nil { if err != nil {
return fmt.Errorf("Retreiving database info: %w", err) return "", fmt.Errorf("Retreiving database info: %w", err)
} }
f.propertiesBox.SetText(infostr) return infostr, nil
} else {
return "Database " + bucketPath[0], nil
}
}
func (ld *redisLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
ctx := context.Background()
if len(bucketPath) == 0 {
// Leave data tab disabled (default behaviour) // Leave data tab disabled (default behaviour)
return nil return nil
@@ -133,12 +139,12 @@ func (ld *redisLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string
return fmt.Errorf("Listing keys in database %q: %w", bucketPath[0], err) return fmt.Errorf("Listing keys in database %q: %w", bucketPath[0], err)
} }
f.propertiesBox.SetText(fmt.Sprintf("Database %s\nTotal keys: %d\n", bucketPath[0], len(allKeys))) // Redis always uses Key string, Type string, Value []byte as the columns
// Redis always uses Key + Value as the columns f.SetupColumns(
[]columnType{columnType_popupData, columnType_inlineText, columnType_popupData},
f.contentBox.SetColumnCount(3) []string{"Key", "Type", "Value"},
f.contentBox.SetHorizontalHeaderLabels([]string{"Key", "Type", "Value"}) )
for _, key := range allKeys { for _, key := range allKeys {
typeName, err := ld.db.Type(ctx, key).Result() typeName, err := ld.db.Type(ctx, key).Result()
@@ -146,10 +152,9 @@ func (ld *redisLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string
return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err) return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err)
} }
rpos := f.contentBox.RowCount() rpos := f.AddRow()
f.contentBox.SetRowCount(rpos + 1) f.SetCell(rpos, 0, key)
f.contentBox.SetItem(rpos, 0, qt.NewQTableWidgetItem2(formatUtf8([]byte(key)))) f.SetCell(rpos, 1, typeName)
f.contentBox.SetItem(rpos, 1, qt.NewQTableWidgetItem2(typeName))
switch typeName { switch typeName {
case "string": case "string":
@@ -157,7 +162,8 @@ func (ld *redisLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string
if err != nil { if err != nil {
return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err) return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err)
} }
f.contentBox.SetItem(rpos, 2, qt.NewQTableWidgetItem2(val))
f.SetCell(rpos, 2, []byte(val))
case "hash": case "hash":
val, err := ld.db.HGetAll(ctx, key).Result() val, err := ld.db.HGetAll(ctx, key).Result()
@@ -165,7 +171,7 @@ func (ld *redisLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string
return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err) return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err)
} }
// It's a map[string]string // It's a map[string]string
f.contentBox.SetItem(rpos, 2, qt.NewQTableWidgetItem2(formatAny(val))) f.SetCell(rpos, 2, []byte(formatAny(val)))
case "lists": case "lists":
fallthrough fallthrough
@@ -177,14 +183,13 @@ func (ld *redisLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string
fallthrough fallthrough
default: default:
f.contentBox.SetItem(rpos, 2, qt.NewQTableWidgetItem2("<<<other object type>>>")) f.SetCell(rpos, 2, []byte("<<<other object type>>>"))
} }
} }
// Valid // Valid
f.contentBox.ResizeColumnsToContents() f.Ready()
f.contentBox.SetEnabled(true)
return nil return nil
} else { } else {
@@ -219,7 +224,7 @@ func (ld *redisLoadedDatabase) NavContext(bucketPath []string) ([]contextAction,
return nil, nil // No special actions are supported return nil, nil // No special actions are supported
} }
func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *qt.QTableWidget) error { func (ld *redisLoadedDatabase) ExecQuery(query string, _ []string, resultArea *tableState) error {
ctx := context.Background() ctx := context.Background()
// Need to parse the query into separate string+args fields for the protocol // Need to parse the query into separate string+args fields for the protocol
@@ -235,32 +240,23 @@ func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *qt.QTableWidg
return fmt.Errorf("The redis query returned an error: %w", err) return fmt.Errorf("The redis query returned an error: %w", err)
} }
vcl_clear_grid(resultArea) resultArea.SetupColumns([]columnType{columnType_inlineText}, []string{"Result"})
resultArea.SetColumnCount(1)
resultArea.SetHorizontalHeaderLabels([]string{"Result"})
// The result is probably a single value or a string slice // The result is probably a single value or a string slice
switch ret := ret.(type) { switch ret := ret.(type) {
case []string: case []string:
// Multiple values // Multiple values
for _, single := range ret { for _, single := range ret {
resultArea.AddRowData(single)
rpos := resultArea.RowCount()
resultArea.SetRowCount(rpos + 1)
resultArea.SetItem(rpos, 0, qt.NewQTableWidgetItem2(formatUtf8([]byte(single))))
} }
default: default:
// Single value // Single value
rpos := resultArea.RowCount() // Unknown object type
resultArea.SetRowCount(rpos + 1) resultArea.AddRowData(formatAny(ret)) // formatUtf8
resultArea.SetItem(rpos, 0, qt.NewQTableWidgetItem2(formatAny(ret))) // formatUtf8
} }
resultArea.ResizeColumnsToContents() resultArea.Ready()
resultArea.SetEnabled(true)
return nil return nil
} }

74
db_rosedb.go Normal file
View File

@@ -0,0 +1,74 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mappu/autoconfig"
"github.com/rosedblabs/rosedb/v2"
)
type roseLdb struct {
db *rosedb.DB
}
func (ld *roseLdb) DriverName() string {
return "RoseDB"
}
func (ld *roseLdb) Properties(bucketPath []string) (string, error) {
stats := ld.db.Stat()
return fmt.Sprintf("Statistics: %#v\n", stats), nil
}
func (ld *roseLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"})
ld.db.Ascend(func(k, v []byte) (bool, error) {
f.AddRow_PK_Data(k, k, v)
return true, nil
})
f.Ready()
return nil
}
func (n *roseLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return kvstore_ApplyChanges(f, n.db.Put, n.db.Delete)
}
func (ld *roseLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *roseLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No supported actions
}
func (ld *roseLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &roseLdb{} // interface assertion
var _ editableLoadedDatabase = &roseLdb{} // interface assertion
//
type roseDBConn struct {
Directory autoconfig.ExistingDirectory
}
func (c *roseDBConn) Connect(ctx context.Context) (loadedDatabase, string, error) {
options := rosedb.DefaultOptions // copy
options.DirPath = string(c.Directory)
db, err := rosedb.Open(options)
if err != nil {
return nil, "", err
}
return &roseLdb{db: db}, filepath.Base(string(c.Directory)), nil
}

189
db_secretsvc_linux.go Normal file
View File

@@ -0,0 +1,189 @@
package main
import (
"context"
"errors"
"fmt"
"strings"
dbus "github.com/godbus/dbus/v5"
qt "github.com/mappu/miqt/qt6"
secretservice "github.com/zalando/go-keyring/secret_service"
)
type secretServiceDb struct {
svc *secretservice.SecretService
session dbus.BusObject
}
func (ld *secretServiceDb) DriverName() string {
return "FreeDesktop.org Secret Service"
}
func (ld *secretServiceDb) Properties(bucketPath []string) (string, error) {
return "", nil // No properties
}
func (ld *secretServiceDb) RenderForNav(f *tableState, bucketPath []string) error {
const (
collectionBasePath = "/org/freedesktop/secrets/collection/"
itemInterface = "org.freedesktop.Secret.Item"
serviceName = "org.freedesktop.secrets"
)
if len(bucketPath) == 0 {
// No data
} else if len(bucketPath) == 1 {
f.SetupColumns(
[]columnType{columnType_inlineText, columnType_inlineText, columnType_inlineText, columnType_inlineText, columnType_inlineText, columnType_popupData},
[]string{"ID", "Label", "Attributes", "ContentType", "Parameters", "Value"},
)
// Collection is selected
collection := ld.svc.GetCollection(bucketPath[0])
// Perform an empty search to find all items
allItems, err := ld.svc.SearchItems(collection, map[string]string{})
if err != nil {
return err
}
for _, item := range allItems {
//
obj := ld.svc.Object(serviceName, item)
label, err := obj.GetProperty(itemInterface + ".Label")
if err != nil {
return fmt.Errorf("Reading label %q: %w", item, err)
}
// Attributes: a map[string]string{}
// Is this optional?
// Calling attrs.String() gives a JSON representation
attrs, err := obj.GetProperty(itemInterface + ".Attributes")
if err != nil {
return fmt.Errorf("Reading attributes %q: %w", item, err)
}
secr, err := ld.svc.GetSecret(item, ld.session.Path())
if err != nil {
return fmt.Errorf("Reading secret %q: %w", item, err)
}
f.AddRowData(
string(item), // ID
label.Value().(string), // Label
attrs.String(), // Attributes (JSON)
secr.ContentType, // ContentType
string(secr.Parameters), // Parameters
secr.Value, // Value - []byte
)
}
// Valid
f.Ready()
}
return nil
}
func (ld *secretServiceDb) listCollections() ([]string, error) {
const (
serviceName = "org.freedesktop.secrets"
servicePath = "/org/freedesktop/secrets"
collectionsInterface = "org.freedesktop.Secret.Service.Collections"
collectionBasePath = "/org/freedesktop/secrets/collection/"
)
obj := ld.svc.Conn.Object(serviceName, servicePath)
val, err := obj.GetProperty(collectionsInterface)
if err != nil {
return nil, err
}
paths := val.Value().([]dbus.ObjectPath)
ret := make([]string, 0, len(paths))
for _, p := range paths {
// They are expected to have {collectionBasePath} as prefix
colName := string(p)
if strings.HasPrefix(colName, collectionBasePath) {
colName = colName[len(collectionBasePath):]
}
ret = append(ret, colName)
}
return ret, nil
}
func (ld *secretServiceDb) NavChildren(bucketPath []string) ([]string, error) {
if len(bucketPath) == 0 {
return ld.listCollections()
}
return []string{}, nil // No children
}
func (ld *secretServiceDb) newCollection(sender *qt.QTreeWidgetItem, bucketPath []string) error {
name := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new collection:")
if name == "" {
return nil // cancelled
}
_, err := ld.svc.CreateCollection(name)
return err
}
func (ld *secretServiceDb) unlockKeychain(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if len(bucketPath) != 1 {
return errors.New("Invalid selection")
}
coll := ld.svc.GetCollection(bucketPath[0])
return ld.svc.Unlock(coll.Path())
}
func (ld *secretServiceDb) NavContext(bucketPath []string) ([]contextAction, error) {
if len(bucketPath) == 0 {
return []contextAction{
contextAction{"Create collection...", ld.newCollection},
}, nil
} else if len(bucketPath) == 1 {
return []contextAction{
contextAction{"Unlock", ld.unlockKeychain},
}, nil
} else {
return nil, nil // Unreachable
}
}
func (ld *secretServiceDb) Close() {
_ = ld.svc.Close(ld.session)
}
var _ loadedDatabase = &secretServiceDb{} // interface assertion
//
type secretServiceConnection struct {
}
func (ssc *secretServiceConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
svc, err := secretservice.NewSecretService()
if err != nil {
return nil, "", err
}
session, err := svc.OpenSession()
if err != nil {
return nil, "", err
}
return &secretServiceDb{svc: svc, session: session}, "dbus://SessionBus/org.freedesktop.secrets", nil
}

19
db_secretsvc_other.go Normal file
View File

@@ -0,0 +1,19 @@
//go:build !linux
// +build !linux
package main
import (
"context"
"errors"
"github.com/mappu/autoconfig"
)
type secretServiceConnection struct {
H1 autoconfig.Header `ylabel:"Not supported on this operating system"`
}
func (ssc *secretServiceConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
return nil, "", errors.New("Not supported on this operating system")
}

View File

@@ -8,7 +8,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"yvbolt/sqliteclidriver" "qbolt/sqliteclidriver"
"github.com/mappu/autoconfig" "github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6" qt "github.com/mappu/miqt/qt6"
@@ -32,22 +32,12 @@ func (ld *sqliteLoadedDatabase) DriverName() string {
return "SQLite " + ver1 return "SQLite " + ver1
} }
func (ld *sqliteLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []string) error { func (ld *sqliteLoadedDatabase) Properties(bucketPath []string) (string, error) {
if len(bucketPath) == 0 || len(bucketPath) == 1 {
return "Please select...", nil // No properties
ctx := context.TODO() } else {
if len(bucketPath) == 0 {
// Top-level
f.propertiesBox.SetText("Please select...")
return nil
} else if len(bucketPath) == 1 {
// Category (tables, ...)
f.propertiesBox.SetText("Please select...")
return nil
} else if len(bucketPath) == 2 && bucketPath[0] == sqliteTablesCaption {
// Render for specific table
tableName := bucketPath[1] tableName := bucketPath[1]
// Get some basic properties // Get some basic properties
@@ -59,7 +49,24 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []strin
} }
// Display table properties // Display table properties
f.propertiesBox.SetText(fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt)) return fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt), nil
}
}
func (ld *sqliteLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
ctx := context.TODO()
if len(bucketPath) == 0 {
return nil
} else if len(bucketPath) == 1 {
// Category (tables, ...)
return nil
} else if len(bucketPath) == 2 && bucketPath[0] == sqliteTablesCaption {
// Render for specific table
tableName := bucketPath[1]
// Load column details // Load column details
// Use SELECT form instead of common PRAGMA table_info so we can just get names // Use SELECT form instead of common PRAGMA table_info so we can just get names
@@ -70,7 +77,7 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []strin
return fmt.Errorf("Failed to load columns for table %q: %w", tableName, err) return fmt.Errorf("Failed to load columns for table %q: %w", tableName, err)
} }
populateColumns(columnNames, f.contentBox) populateColumns(columnNames, f)
// Find primary key, if any // Find primary key, if any
var primaryKeyIdx int = -1 var primaryKeyIdx int = -1
@@ -89,11 +96,14 @@ func (ld *sqliteLoadedDatabase) RenderForNav(f *MainWindowUi, bucketPath []strin
return fmt.Errorf("Failed to load data for table %q: %w", tableName, err) return fmt.Errorf("Failed to load data for table %q: %w", tableName, err)
} }
defer datar.Close() defer datar.Close()
populateRows(datar, f.contentBox, primaryKeyIdx)
err = populateRows(datar, f, primaryKeyIdx)
if err != nil {
return err
}
// We successfully populated the data grid // We successfully populated the data grid
f.contentBox.ResizeColumnsToContents() f.Ready()
f.contentBox.SetEnabled(true)
return nil return nil
} else { } else {
@@ -130,14 +140,14 @@ func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) (
return ret, nil return ret, nil
} }
func populateColumns(names []string, dest *qt.QTableWidget) { func populateColumns(names []string, dest *tableState) {
dest.SetColumnCount(len(names)) // FIXME better column types?
dest.SetHorizontalHeaderLabels(names) dest.SetupColumns(slice_repeat(columnType_inlineText, len(names)), names)
} }
func populateRows(rr *sql.Rows, dest *qt.QTableWidget, pk_index int) { func populateRows(rr *sql.Rows, dest *tableState, pk_index int) error {
numColumns := int(dest.ColumnCount()) numColumns := len(dest.columns)
for rr.Next() { for rr.Next() {
fields := make([]interface{}, numColumns) fields := make([]interface{}, numColumns)
@@ -148,34 +158,23 @@ func populateRows(rr *sql.Rows, dest *qt.QTableWidget, pk_index int) {
err := rr.Scan(pfields...) err := rr.Scan(pfields...)
if err != nil { if err != nil {
qt.QMessageBox_Warning(dest.QWidget, APPNAME, fmt.Sprintf("Failed to load data: %s", err.Error())) return fmt.Errorf("Scan: %w", err)
return
} }
rpos := dest.RowCount() rpos := dest.AddRow()
dest.SetRowCount(rpos + 1)
for i := 0; i < len(fields); i += 1 { for i := 0; i < len(fields); i += 1 {
dest.SetCell(rpos, i, formatAny(fields[i])) // FIXME stop doing string conversion here
}
cellItem := qt.NewQTableWidgetItem2(formatAny(fields[i])) if pk_index >= 0 {
if i == pk_index { dest.SetRowPrimaryKey(rpos, []byte(formatAny(fields[pk_index]))) // FIXME stop doing string conversion here
cellItem.SetData(DataPrimaryKeyRole, qt.NewQVariant11(formatAny(fields[i]))) // FIXME stop doing string conversion here
} else if pk_index < 0 {
// We do not have a primary key for this table
// Therefore - disable editing
cellItem.SetFlags(cellItem.Flags() & ^qt.ItemIsEditable)
}
dest.SetItem(rpos, i, cellItem)
} }
} }
if rr.Err() != nil { return rr.Err()
qt.QMessageBox_Warning(dest.QWidget, APPNAME, fmt.Sprintf("Failed to load data: %s", rr.Err().Error()))
return
}
} }
func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *qt.QTableWidget) error { func (ld *sqliteLoadedDatabase) ExecQuery(query string, _ []string, resultArea *tableState) error {
rr, err := ld.db.Query(query) rr, err := ld.db.Query(query)
if err != nil { if err != nil {
return err return err
@@ -183,8 +182,6 @@ func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *qt.QTableWid
defer rr.Close() defer rr.Close()
vcl_clear_grid(resultArea)
columns, err := rr.Columns() columns, err := rr.Columns()
if err != nil { if err != nil {
return err return err
@@ -192,11 +189,12 @@ func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *qt.QTableWid
populateColumns(columns, resultArea) populateColumns(columns, resultArea)
populateRows(rr, resultArea, -1) err = populateRows(rr, resultArea, -1)
if err != nil {
return err
}
resultArea.ResizeColumnsToContents() resultArea.Ready()
resultArea.SetEnabled(true)
resultArea.EditTriggers()
return nil return nil
} }
@@ -210,17 +208,13 @@ func (n *sqliteLoadedDatabase) getPrimaryKeyForTable(ctx context.Context, tx Row
return primaryColumnName, err return primaryColumnName, err
} }
func (n *sqliteLoadedDatabase) ApplyChanges(f *App, bucketPath []string) (retErr error) { func (n *sqliteLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) (retErr error) {
if len(bucketPath) != 2 { if len(bucketPath) != 2 {
return errors.New("invalid selection") return errors.New("invalid selection")
} }
tableName := bucketPath[1] tableName := bucketPath[1]
// We have rendered row IDs, need to convert back to an SQLite primary key
// TODO stash the real key inside f.ui.contentBox.Objects()
// FIXME breaks if you try and edit the primary key(!) because the previous
ctx := context.Background() ctx := context.Background()
tx, err := n.db.BeginTx(ctx, nil) tx, err := n.db.BeginTx(ctx, nil)
@@ -238,24 +232,12 @@ func (n *sqliteLoadedDatabase) ApplyChanges(f *App, bucketPath []string) (retErr
} }
}() }()
// Data grid properties
var columnNames []string
for i := 0; i < f.ui.contentBox.ColumnCount(); i++ {
columnNames = append(columnNames, f.ui.contentBox.HorizontalHeaderItem(i).Text())
}
// Query sqlite table metadata to determine which of these is the PRIMARY KEY // Query sqlite table metadata to determine which of these is the PRIMARY KEY
primaryColumnName, err := n.getPrimaryKeyForTable(ctx, tx, tableName) primaryColumnName, err := n.getPrimaryKeyForTable(ctx, tx, tableName)
if err != nil { if err != nil {
return fmt.Errorf("Finding primary key for update: %w", err) return fmt.Errorf("Finding primary key for update: %w", err)
} }
// Convert it to an index
primaryColumnIdx, ok := slice_find(columnNames, primaryColumnName)
if !ok {
return fmt.Errorf("Primary key %q missing from available columns", primaryColumnName)
}
// SQLite can only LIMIT 1 on update/delete if it was compiled with // SQLite can only LIMIT 1 on update/delete if it was compiled with
// SQLITE_ENABLE_UPDATE_DELETE_LIMIT, which isn't the case for the mattn // SQLITE_ENABLE_UPDATE_DELETE_LIMIT, which isn't the case for the mattn
// cgo library // cgo library
@@ -263,22 +245,20 @@ func (n *sqliteLoadedDatabase) ApplyChanges(f *App, bucketPath []string) (retErr
// Edit // Edit
for rowid, editcells := range f.updateRows { for rowid, editcells := range f.updateRows {
stmt := `UPDATE "` + tableName + `" SET ` stmt := `UPDATE [` + tableName + `] SET `
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind) params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind)
for ct, cell := range editcells { for ct, cell := range editcells {
if ct > 0 { if ct > 0 {
stmt += `, ` stmt += `, `
} }
stmt += `"` + columnNames[cell] + `" = ?` stmt += `[` + f.columnLabels[cell] + `] = ?`
params = append(params, f.ui.contentBox.Item(rowid, cell).Text()) params = append(params, (f.columns[cell].(*stringColumn)).vals[rowid]) // FIXME stop doing string conversion
} }
stmt += ` WHERE "` + primaryColumnName + `" = ?` stmt += ` WHERE [` + primaryColumnName + `] = ?`
// The primary key was stashed in DataPrimaryKeyRole, on the pk_idx'th // Update by primary key (stored separately)
// column, as a QString qvariant pkVal := string(f.primaryKeys[rowid]) // FIXME avoid string marshalling
// FIXME avoid string marshalling
pkVal := f.ui.contentBox.Item(rowid, primaryColumnIdx).Data(DataPrimaryKeyRole).ToString()
params = append(params, pkVal) params = append(params, pkVal)
_, err = tx.ExecContext(ctx, stmt, params...) _, err = tx.ExecContext(ctx, stmt, params...)
@@ -289,9 +269,9 @@ func (n *sqliteLoadedDatabase) ApplyChanges(f *App, bucketPath []string) (retErr
// Delete by key (affects rowids after re-render) // Delete by key (affects rowids after re-render)
for rowid, _ := range f.deleteRows { for rowid, _ := range f.deleteRows {
pkVal := f.ui.contentBox.Item(rowid, primaryColumnIdx).Text() pkVal := string(f.primaryKeys[rowid]) // FIXME avoid string marshalling
stmt := `DELETE FROM "` + tableName + `" WHERE "` + primaryColumnName + `" = ?` stmt := `DELETE FROM [` + tableName + `] WHERE [` + primaryColumnName + `] = ?`
_, err = tx.ExecContext(ctx, stmt, pkVal) _, err = tx.ExecContext(ctx, stmt, pkVal)
if err != nil { if err != nil {
@@ -301,15 +281,15 @@ func (n *sqliteLoadedDatabase) ApplyChanges(f *App, bucketPath []string) (retErr
// Insert all new entries // Insert all new entries
for rowid, _ := range f.insertRows { for rowid, _ := range f.insertRows {
stmt := `INSERT INTO "` + tableName + `" (` + strings.Join(columnNames, `, `) + `) VALUES (` stmt := `INSERT INTO [` + tableName + `] ([` + strings.Join(f.columnLabels, `], [`) + `]) VALUES (`
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind) params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind)
for colid := 0; colid < len(columnNames); colid++ { for colid := 0; colid < len(f.columnLabels); colid++ {
if colid > 0 { if colid > 0 {
stmt += `, ` stmt += `, `
} }
stmt += "?" stmt += "?"
params = append(params, f.ui.contentBox.Item(rowid, colid).Text()) params = append(params, (f.columns[colid].(*stringColumn)).vals[rowid]) // FIXME stop doing string conversion
} }
stmt += `)` stmt += `)`
@@ -385,7 +365,7 @@ func (ld *sqliteLoadedDatabase) CompactDatabase(sender *qt.QTreeWidgetItem, buck
func (ld *sqliteLoadedDatabase) ExportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error { func (ld *sqliteLoadedDatabase) ExportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error {
// Popup for output file // Popup for output file
savePath := qt.QFileDialog_GetSaveFileName4(sender.TreeWidget().QWidget, "Save backup as...", "", "SQLite database (*.db *.db3 *.sqlite *.sqlite3);;All files (*.*)") savePath := qt.QFileDialog_GetSaveFileName4(sender.TreeWidget().QWidget, "Save backup as...", "", "SQLite database (*.db *.db3 *.sqlite *.sqlite3);;All files (*)")
if savePath == "" { if savePath == "" {
return nil // cancelled return nil // cancelled
} }
@@ -403,7 +383,7 @@ func (ld *sqliteLoadedDatabase) DropTable(sender *qt.QTreeWidgetItem, bucketPath
tableName := bucketPath[1] tableName := bucketPath[1]
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, "Drop table", fmt.Sprintf("Are you sure you want to drop the table %q?", tableName)) { if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to drop the table %q?", tableName)) {
return nil // cancelled return nil // cancelled
} }
@@ -422,37 +402,55 @@ var _ queryableLoadedDatabase = &sqliteLoadedDatabase{} // interface assertion
// //
type sqliteConnection struct { type sqliteConnection struct {
Database autoconfig.ExistingFile `yfilter:"SQLite database (*.db *.db3 *.sqlite *.sqlite3);;All files (*.*)"` Type autoconfig.OneOf
CliDriver bool `ylabel:"Use experimental CLI driver"` Disk *struct {
Database autoconfig.ExistingFile `yfilter:"SQLite database (*.db *.db3 *.sqlite *.sqlite3);;All files (*)"`
CliDriver bool `ylabel:"Use experimental CLI driver"`
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
SSH *struct {
SSHServer *SSHTunnel
Database string
} `json:",omitempty"`
} }
func (sc *sqliteConnection) Connect(ctx context.Context) (loadedDatabase, string, error) { func (sc *sqliteConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
if sc.Disk != nil {
driver := "sqlite3"
if sc.Disk.CliDriver {
driver = "sqliteclidriver"
}
driver := "sqlite3" db, err := sql.Open(driver, string(sc.Disk.Database))
if sc.CliDriver { if err != nil {
driver = "sqliteclidriver" return nil, "", err
}
return &sqliteLoadedDatabase{db: db}, filepath.Base(string(sc.Disk.Database)), nil
} else if sc.Memory != nil { // memory
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
return nil, "", err
}
return &sqliteLoadedDatabase{db: db}, ":memory:", nil
} else if sc.SSH != nil {
if sc.SSH.SSHServer == nil {
return nil, "", errors.New("Invalid configuration")
}
cl, err := sc.SSH.SSHServer.Open(ctx)
if err != nil {
return nil, "", err
}
db := sqliteclidriver.OpenSSH(cl, sc.SSH.Database)
return &sqliteLoadedDatabase{db: db}, "SSH[" + filepath.Base(string(sc.SSH.Database)) + "]", nil
} else {
return nil, "", errors.New("Invalid configuration")
} }
db, err := sql.Open(driver, string(sc.Database))
if err != nil {
return nil, "", err
}
return &sqliteLoadedDatabase{db: db}, filepath.Base(string(sc.Database)), nil
}
type sqliteMemoryConnection struct{}
func (sc *sqliteMemoryConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
return nil, "", err
}
return &sqliteLoadedDatabase{db: db}, ":memory:", nil
}
func init() {
registerDatabase(&sqliteConnection{}, "SQLite", `:/assets/vendor_sqlite.png`)
registerDatabase(&sqliteMemoryConnection{}, "SQLite (Memory)", `:/assets/vendor_sqlite.png`)
} }

179
db_sshagent.go Normal file
View File

@@ -0,0 +1,179 @@
package main
import (
"context"
"fmt"
"net"
"net/netip"
"os"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"golang.org/x/crypto/ssh/agent"
)
type sshAgentLdb struct {
conn agent.ExtendedAgent
}
func (ld *sshAgentLdb) DriverName() string {
return "ssh-agent"
}
func (ld *sshAgentLdb) Properties(bucketPath []string) (string, error) {
return "", nil
}
func (ld *sshAgentLdb) RenderForNav(f *tableState, bucketPath []string) error {
keys, err := ld.conn.List()
if err != nil {
return err
}
f.SetupColumns([]columnType{columnType_inlineText, columnType_inlineText, columnType_popupData}, []string{"Comment", "Type", "Public Key"})
for _, key := range keys {
// The publicKey blob is the effective primary-key for DB manipulation
f.AddRow_PK_Data(key.Blob, key.Comment, key.Format, key.Blob)
}
f.Ready()
return nil
}
func (ld *sshAgentLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *sshAgentLdb) lockPrompt(sender *qt.QTreeWidgetItem, bucketPath []string) error {
parent := sender.TreeWidget().QWidget
props := encryptionKey{}
autoconfig.OpenDialog(&props, parent, "Enter lock password...", func() {
key, err := props.Get()
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, err.Error())
return
}
if len(key) == 0 {
// Cancelled
return
}
err = ld.conn.Lock(key)
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, "Locking SSH agent: "+err.Error())
return
}
})
return nil // n.b. refreshes now
}
func (ld *sshAgentLdb) unlockPrompt(sender *qt.QTreeWidgetItem, bucketPath []string) error {
parent := sender.TreeWidget().QWidget
props := encryptionKey{}
autoconfig.OpenDialog(&props, parent, "Enter unlock password...", func() {
key, err := props.Get()
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, err.Error())
return
}
if len(key) == 0 {
// Cancelled
return
}
err = ld.conn.Unlock(key)
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, "Unlocking SSH agent: "+err.Error())
return
}
// Trigger a refresh
})
return nil // n.b. refreshes now, which may cause double-error if we are still locked
}
func (ld *sshAgentLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{"Lock agent...", ld.lockPrompt},
{"Unlock agent...", ld.unlockPrompt},
}, nil
}
func (ld *sshAgentLdb) Close() {
// noop
}
var _ loadedDatabase = &sshAgentLdb{} // interface assertion
//
type sshAgentConn struct {
Type autoconfig.OneOf
Unix *autoconfig.ExistingFile `json:",omitempty"`
TCP *autoconfig.AddressPort `json:",omitempty"`
}
func (c *sshAgentConn) Reset() {
if sshAuthSock := os.Getenv("SSH_AUTH_SOCK"); sshAuthSock != "" {
if _, err := os.Stat(sshAuthSock); err == nil {
// File
c.Type = "Unix"
val := autoconfig.ExistingFile(sshAuthSock)
c.Unix = &val
} else if props, err := netip.ParseAddrPort(sshAuthSock); err == nil {
// IP:Port
c.Type = "TCP"
val := autoconfig.AddressPort{
Address: props.Addr().String(),
Port: int(props.Port()),
}
c.TCP = &val
} else {
// Can't parse env var
}
}
}
func (c *sshAgentConn) getAgent() (agent.ExtendedAgent, error) {
if c.Unix != nil {
conn, err := net.Dial("unix", string(*c.Unix))
if err != nil {
return nil, fmt.Errorf("Connecting to SSH agent %q: %w", c.Unix, err)
}
return agent.NewClient(conn), nil
} else if c.TCP != nil {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.TCP.Address, c.TCP.Port))
if err != nil {
return nil, fmt.Errorf("Connecting to SSH agent %q: %w", c.TCP.String(), err)
}
return agent.NewClient(conn), nil
} else {
return nil, fmt.Errorf("No connection details specified")
}
}
func (c *sshAgentConn) Connect(ctx context.Context) (loadedDatabase, string, error) {
agent, err := c.getAgent()
if err != nil {
return nil, "", err
}
return &sshAgentLdb{conn: agent}, "SSH Agent", nil
}

115
db_starskey.go Normal file
View File

@@ -0,0 +1,115 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mappu/autoconfig"
"github.com/starskey-io/starskey"
)
type starskeyLdb struct {
db *starskey.Starskey
}
func (ld *starskeyLdb) DriverName() string {
return "Starskey"
}
func (ld *starskeyLdb) Properties(bucketPath []string) (string, error) {
return "", nil // No properties
}
func (ld *starskeyLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
// Starskey always uses Key + Value as the columns
f.SetupColumns([]columnType{columnType_popupData, columnType_popupData}, []string{"Key", "Value"})
// It's possible to create a transaction in Starskey, but you can't enumerate keys
// within the transaction - doesn't really help us
var allKeys [][]byte
_, err := ld.db.FilterKeys(func(key []byte) bool {
allKeys = append(allKeys, slice_dup(key))
return false // don't get value in here
})
if err != nil {
return fmt.Errorf("FilterKeys: %w", err)
}
for _, key := range allKeys {
val, err := ld.db.Get(key)
if err != nil {
// We get <nil, nil> if not found, so any error is a real error
return fmt.Errorf("Reading key %q: %w", string(key), err)
}
if val == nil {
// Key not found
// The hack to use FilterKeys() means this can happen if the key is
// pointing to a tombstone
continue
}
f.AddRow_PK_Data(key, key, val)
}
// Valid
f.Ready()
return nil
}
func (n *starskeyLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return kvstore_ApplyChanges(f, n.db.Put, n.db.Delete)
}
func (ld *starskeyLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *starskeyLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *starskeyLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &starskeyLdb{} // interface assertion
var _ editableLoadedDatabase = &starskeyLdb{} // interface assertion
//
type starskeyConnection struct {
Directory autoconfig.ExistingDirectory
Compression autoconfig.EnumList `yenum:"No compression;;Snappy;;S2"`
}
func (pdc *starskeyConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
cfg := starskey.Config{
Permission: 0755,
Directory: string(pdc.Directory),
FlushThreshold: (1024 * 1024) * 24, // Upstream default (24 MiB)
MaxLevel: 3, // Upstream default
SizeFactor: 10, // Upstream default
}
if pdc.Compression == 0 {
} else if pdc.Compression == 1 {
cfg.Compression = true
cfg.CompressionOption = starskey.SnappyCompression
} else if pdc.Compression == 2 {
cfg.Compression = true
cfg.CompressionOption = starskey.S2Compression
}
db, err := starskey.Open(&cfg)
if err != nil {
return nil, "", err
}
return &starskeyLdb{db: db}, filepath.Base(string(pdc.Directory)), nil
}

View File

@@ -18,11 +18,26 @@ type Entry struct {
Properties [][2]string Properties [][2]string
} }
type Application struct {
Name string
Entries []Entry
}
type Database struct { type Database struct {
Entries []Entry Entries []Application
AllColumnNames []string AllColumnNames []string
} }
func (d *Database) FindApplicationByName(search string) (*Application, bool) {
for idx, app := range d.Entries {
if app.Name == search {
return &d.Entries[idx], true // TODO faster lookup?
}
}
return nil, false
}
func Parse(r io.Reader) (*Database, error) { func Parse(r io.Reader) (*Database, error) {
sc := bufio.NewScanner(r) sc := bufio.NewScanner(r)
@@ -87,8 +102,31 @@ func Parse(r io.Reader) (*Database, error) {
entries = append(entries, wip) entries = append(entries, wip)
} }
// Group all entries by Application
apps := make([]Application, 0)
appIndexes := make(map[string]int, 0)
for _, entry := range entries {
spos := strings.Index(entry.Name, `/`)
appName := entry.Name[0:spos]
idx, ok := appIndexes[appName]
if !ok {
appIndexes[appName] = len(apps)
apps = append(apps, Application{
Name: appName,
Entries: []Entry{entry},
})
} else {
tmp := apps[idx]
tmp.Entries = append(tmp.Entries, entry)
apps[idx] = tmp
}
}
return &Database{ return &Database{
Entries: entries, Entries: apps,
AllColumnNames: discoveredColumns, AllColumnNames: discoveredColumns,
}, nil }, nil
} }

BIN
doc/screenshot-000.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -3,14 +3,21 @@
<file>assets/add.png</file> <file>assets/add.png</file>
<file>assets/arrow_refresh.png</file> <file>assets/arrow_refresh.png</file>
<file>assets/chart_bar.png</file> <file>assets/chart_bar.png</file>
<file>assets/compress.png</file>
<file>assets/connect.png</file>
<file>assets/database.png</file> <file>assets/database.png</file>
<file>assets/database_add.png</file> <file>assets/database_add.png</file>
<file>assets/database_delete.png</file> <file>assets/database_delete.png</file>
<file>assets/database_key.png</file>
<file>assets/database_lightning.png</file> <file>assets/database_lightning.png</file>
<file>assets/database_save.png</file> <file>assets/database_save.png</file>
<file>assets/delete.png</file> <file>assets/delete.png</file>
<file>assets/disconnect.png</file>
<file>assets/help.png</file>
<file>assets/key.png</file>
<file>assets/lightning.png</file> <file>assets/lightning.png</file>
<file>assets/lightning_go.png</file> <file>assets/lightning_go.png</file>
<file>assets/page_key.png</file>
<file>assets/pencil.png</file> <file>assets/pencil.png</file>
<file>assets/pencil_add.png</file> <file>assets/pencil_add.png</file>
<file>assets/pencil_delete.png</file> <file>assets/pencil_delete.png</file>
@@ -20,13 +27,23 @@
<file>assets/table_add.png</file> <file>assets/table_add.png</file>
<file>assets/table_delete.png</file> <file>assets/table_delete.png</file>
<file>assets/table_save.png</file> <file>assets/table_save.png</file>
<file>assets/vendor_buntdb.png</file>
<file>assets/vendor_cockroach.png</file> <file>assets/vendor_cockroach.png</file>
<file>assets/vendor_debian.png</file> <file>assets/vendor_debian.png</file>
<file>assets/vendor_dgraph.png</file> <file>assets/vendor_dgraph.png</file>
<file>assets/vendor_freedesktop.png</file>
<file>assets/vendor_github.png</file> <file>assets/vendor_github.png</file>
<file>assets/vendor_leveldb.png</file> <file>assets/vendor_leveldb.png</file>
<file>assets/vendor_lmdb.png</file>
<file>assets/vendor_lotus.png</file>
<file>assets/vendor_mongodb.png</file>
<file>assets/vendor_mysql.png</file> <file>assets/vendor_mysql.png</file>
<file>assets/vendor_qt.png</file>
<file>assets/vendor_redis.png</file> <file>assets/vendor_redis.png</file>
<file>assets/vendor_riak.png</file>
<file>assets/vendor_rosedb.png</file>
<file>assets/vendor_sqlite.png</file> <file>assets/vendor_sqlite.png</file>
<file>assets/vendor_ssh.png</file>
<file>assets/vendor_starskey.png</file>
</qresource> </qresource>
</RCC> </RCC>

BIN
embed.rcc

Binary file not shown.

71
go.mod
View File

@@ -1,4 +1,4 @@
module yvbolt module qbolt
go 1.24.0 go 1.24.0
@@ -7,56 +7,95 @@ toolchain go1.24.4
require ( require (
github.com/cockroachdb/pebble v1.1.5 github.com/cockroachdb/pebble v1.1.5
github.com/dgraph-io/badger/v4 v4.8.0 github.com/dgraph-io/badger/v4 v4.8.0
github.com/godbus/dbus/v5 v5.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/mappu/autoconfig v0.2.0 github.com/ledgerwatch/lmdb-go v1.18.2
github.com/lotusdblabs/lotusdb/v2 v2.1.0
github.com/mappu/autoconfig v0.4.1
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf
github.com/mattn/go-sqlite3 v1.14.32 github.com/mattn/go-sqlite3 v1.14.32
github.com/redis/go-redis/v9 v9.16.0 github.com/redis/go-redis/v9 v9.17.2
github.com/rosedblabs/rosedb/v2 v2.3.6
github.com/starskey-io/starskey v0.1.9
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/syndtr/goleveldb v1.0.0 github.com/syndtr/goleveldb v1.0.0
github.com/zalando/go-keyring v0.2.6
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
golang.org/x/crypto v0.44.0 go.mills.io/bitcask/v2 v2.1.5
go.mongodb.org/mongo-driver/v2 v2.4.1
golang.org/x/crypto v0.46.0
) )
require ( require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect
github.com/DataDog/zstd v1.5.7 // indirect github.com/DataDog/zstd v1.5.7 // indirect
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cockroachdb/errors v1.12.0 // indirect github.com/cockroachdb/errors v1.12.0 // indirect
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect
github.com/cockroachdb/redact v1.1.6 // indirect github.com/cockroachdb/redact v1.1.6 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/danieljoos/wincred v1.2.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/getsentry/sentry-go v0.36.2 // indirect github.com/getsentry/sentry-go v0.40.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/flatbuffers v25.9.23+incompatible // indirect github.com/google/flatbuffers v25.9.23+incompatible // indirect
github.com/klauspost/compress v1.18.1 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.0.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.2 // indirect github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/robfig/cron/v3 v3.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rosedblabs/diskhash v0.0.0-20230910084041-289755737e2a // indirect
github.com/rosedblabs/wal v1.3.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect
github.com/tidwall/buntdb v1.3.2 // indirect
github.com/tidwall/gjson v1.14.3 // indirect
github.com/tidwall/grect v0.1.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.1.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/net v0.46.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

187
go.sum
View File

@@ -1,11 +1,17 @@
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 h1:uHogIJ9bXH75ZYrXnVShHIyywFiUZ7OOabwd9Sfd8rw=
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81/go.mod h1:6ZvnjTZX1LNo1oLpfaJK8h+MXqHxcBFBIwkgsv+xlv0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
@@ -23,8 +29,12 @@ github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g= github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g=
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs=
github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w=
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk= github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
@@ -36,8 +46,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/getsentry/sentry-go v0.36.2 h1:uhuxRPTrUy0dnSzTd0LrYXlBYygLkKY0hhlG5LXarzM= github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
github.com/getsentry/sentry-go v0.36.2/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c= github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -45,36 +55,59 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/flatbuffers v25.9.23+incompatible h1:rGZKv+wOb6QPzIdkM2KxhBZCDrA0DeN6DNmRDrqIsQU= github.com/google/flatbuffers v25.9.23+incompatible h1:rGZKv+wOb6QPzIdkM2KxhBZCDrA0DeN6DNmRDrqIsQU=
github.com/google/flatbuffers v25.9.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.9.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/go-immutable-radix/v2 v2.0.0 h1:nq9lQ5I71Heg2lRb2/+szuIWKY3Y73d8YKyXyN91WzU=
github.com/hashicorp/go-immutable-radix/v2 v2.0.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mappu/autoconfig v0.1.0 h1:T740iW7/rkqi+pxJIlP+WB433h34jNRjqqG9ABTSScs= github.com/ledgerwatch/lmdb-go v1.18.2 h1:6YKp/KYcqGunNRHKZBBhiYADcIcWKzvu5QZv89RhnFQ=
github.com/mappu/autoconfig v0.1.0/go.mod h1:kca6kaYjqOwkZnY9z5YKo0oI/7Bxe2fMIHhYzXwGHEE= github.com/ledgerwatch/lmdb-go v1.18.2/go.mod h1:NKRpCxksoTQPyxsUcBiVOe0135uqnJsnf6cElxmOL0o=
github.com/mappu/autoconfig v0.2.0 h1:5auhryqiubVBFq9CdY+VHU36bysG70tRGPpyT+M4ycs= github.com/lotusdblabs/lotusdb/v2 v2.1.0 h1:rCBrwED8Po12FzrxxX4zppxoHb2O+sCtddyW4kyDiCQ=
github.com/mappu/autoconfig v0.2.0/go.mod h1:kca6kaYjqOwkZnY9z5YKo0oI/7Bxe2fMIHhYzXwGHEE= github.com/lotusdblabs/lotusdb/v2 v2.1.0/go.mod h1:MyOEvqL3Hxm3HiBOYZ4BlZBnqCIcc2QQkF34VBD76fk=
github.com/mappu/miqt v0.12.0 h1:bBMBDeACmV8TbdLfoN51la7kF6QT3sNAcG+ZdRDgmxU= github.com/mappu/autoconfig v0.4.1 h1:ekO7mzN+beFu7VhNfJxNlL/5wkYcP9PAl9VTG4EDxYs=
github.com/mappu/miqt v0.12.0/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U= github.com/mappu/autoconfig v0.4.1/go.mod h1:kca6kaYjqOwkZnY9z5YKo0oI/7Bxe2fMIHhYzXwGHEE=
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf h1:SmBzNUevLUzu1msJ5xzWH/Kot+GtOtoz0u9la42dRU4= github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf h1:SmBzNUevLUzu1msJ5xzWH/Kot+GtOtoz0u9la42dRU4=
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U= github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U=
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -89,37 +122,101 @@ github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTw
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rosedblabs/diskhash v0.0.0-20230910084041-289755737e2a h1:BNp46nsknQivr3Gxzc6ytzG7xtBscBnLYZIkr0UfCko=
github.com/rosedblabs/diskhash v0.0.0-20230910084041-289755737e2a/go.mod h1:3xvIg+7iOFUL/vMCE/6DwE6Yecb0okVYJBEfpdC/E+8=
github.com/rosedblabs/rosedb/v2 v2.3.6 h1:o8vVOp61hFdORrz/PTosqU21/Z2Bug5I7cy1D3MZh2M=
github.com/rosedblabs/rosedb/v2 v2.3.6/go.mod h1:/de9n2CoYaAGBDxZTJC5Jb0LCQOtoA3GOom+9QD9Z98=
github.com/rosedblabs/wal v1.3.6 h1:oxZYTPX/u4JuGDW98wQ1YamWqerlrlSUFKhgP6Gd/Ao=
github.com/rosedblabs/wal v1.3.6/go.mod h1:wdq54KJUyVTOv1uddMc6Cdh2d/YCIo8yjcwJAb1RCEM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/starskey-io/starskey v0.1.9 h1:lABmD5KQgkpJZTCwSt+BHSOPXe82B9smbuScRL6T8Zk=
github.com/starskey-io/starskey v0.1.9/go.mod h1:qly4ec2C/4Y45jhpL+q4m+Uxzg3mjj0t7RjpJslB3ao=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg=
github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.mills.io/bitcask/v2 v2.1.5 h1:SKPa0TPasJJZ8rNbLDvV3+lRXvdyQ0mwBobm2RH7J7w=
go.mills.io/bitcask/v2 v2.1.5/go.mod h1:ZQFykoTTCvMwy24lBstZhSRQuleYIB4EzWKSOgEv6+k=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver/v2 v2.4.1 h1:hGDMngUao03OVQ6sgV5csk+RWOIkF+CuLsTPobNMGNI=
go.mongodb.org/mongo-driver/v2 v2.4.1/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
@@ -127,41 +224,58 @@ go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -177,5 +291,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -16,16 +16,17 @@ type contextAction struct {
// loadedDatabase is a DB-agnostic interface for each loaded database. // loadedDatabase is a DB-agnostic interface for each loaded database.
type loadedDatabase interface { type loadedDatabase interface {
DriverName() string DriverName() string
RenderForNav(f *MainWindowUi, bucketPath []string) error Properties(bucketPath []string) (string, error)
RenderForNav(f *tableState, bucketPath []string) error
NavChildren(bucketPath []string) ([]string, error) NavChildren(bucketPath []string) ([]string, error)
NavContext(bucketPath []string) ([]contextAction, error) NavContext(bucketPath []string) ([]contextAction, error)
Close() Close()
} }
type queryableLoadedDatabase interface { type queryableLoadedDatabase interface {
ExecQuery(query string, resultArea *qt.QTableWidget) error ExecQuery(query string, bucketPath []string, resultArea *tableState) error
} }
type editableLoadedDatabase interface { type editableLoadedDatabase interface {
ApplyChanges(f *App, bucketPath []string) error ApplyChanges(f *tableState, bucketPath []string) error
} }

378
main.go
View File

@@ -1,40 +1,32 @@
package main package main
import ( import (
"encoding/json" "context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"runtime"
"runtime/debug"
"sort"
"strings" "strings"
"github.com/mappu/miqt/qt6/mainthread"
qt "github.com/mappu/miqt/qt6" qt "github.com/mappu/miqt/qt6"
) )
const ( const (
APPNAME = "yvbolt" APPNAME = "QBolt"
HOMEPAGE_URL = "https://code.ivysaur.me/yvbolt" HOMEPAGE_URL = "https://code.ivysaur.me/qbolt"
CO_INSERT = qt.Yellow
CO_EDIT_IMPLICIT = qt.Green
CO_EDIT_EXPLICIT = qt.DarkGreen
CO_DELETE = qt.Red
) )
type App struct { type App struct {
ui *MainWindowUi ui *MainWindowUi
contentTbl *tableState
resultsTbl *tableState
none *noLoadedDatabase none *noLoadedDatabase
dbs_next int dbs_next int
dbs map[int]loadedDatabase dbs map[int]loadedDatabase
isEditing bool
updateRows map[int][]int // row => []column
insertRows map[int]struct{}
deleteRows map[int]struct{}
} }
func newApp() *App { func newApp() *App {
@@ -44,33 +36,17 @@ func newApp() *App {
a.dbs_next = 0 a.dbs_next = 0
a.dbs = make(map[int]loadedDatabase, 0) a.dbs = make(map[int]loadedDatabase, 0)
a.isEditing = false
a.updateRows = make(map[int][]int, 0)
a.insertRows = make(map[int]struct{}, 0)
a.deleteRows = make(map[int]struct{}, 0)
a.ui = NewMainWindowUi() a.ui = NewMainWindowUi()
a.ui.MainWindow.SetWindowTitle(APPNAME + " " + appVersion)
// "Midlight" is pretty close on Breeze but wrong on Fusion and Windows // Using stylesheet works better than picking a palette colour across all
// AlternateBase ^ // different QStyles
// Window --> too dark a.ui.propertiesBox.SetStyleSheet("background-color: transparent;")
// Button --> too light on Breeze; close on Fusion; too light on Windows
// Light --> too light on Breeze; correct on Fusion; too light on Windows
currentStyle := strings.ToLower(qt.QApplication_Style().Name())
effectiveTabBackground := qt.QPalette__Window // correct on Windows
if currentStyle == "fusion" {
effectiveTabBackground = qt.QPalette__Light
} else if currentStyle == "breeze" {
effectiveTabBackground = qt.QPalette__Midlight // ?? how does this look on dark mode...?
}
a.ui.propertiesBox.Viewport().SetBackgroundRole(effectiveTabBackground)
// //
a.ui.actionConnect.OnTriggered(a.OnMnuConnectClick) a.ui.actionConnect.OnTriggered(a.OnMnuConnectClick)
a.ui.actionConnectionManager.OnTriggered(a.OnMnuConnectionManagerClick)
a.ui.actionE_xit.OnTriggered(a.OnMnuFileExitClick) a.ui.actionE_xit.OnTriggered(a.OnMnuFileExitClick)
@@ -79,6 +55,8 @@ func newApp() *App {
a.ui.mnuDriverVersions.OnTriggered(a.OnMenuHelpVersion) a.ui.mnuDriverVersions.OnTriggered(a.OnMenuHelpVersion)
a.ui.mnuHelpAbout.OnTriggered(a.OnMnuHelpHomepage) a.ui.mnuHelpAbout.OnTriggered(a.OnMnuHelpHomepage)
a.ui.actionCreate_Bolt_database_from_zip.OnTriggered(a.Bolt_ImportZipToDatabase_OnTriggered)
// //
a.ui.Buckets.OnCurrentItemChanged(a.OnNavChange) a.ui.Buckets.OnCurrentItemChanged(a.OnNavChange)
@@ -99,43 +77,43 @@ func newApp() *App {
// //
a.ui.dataRefreshBtn.OnClicked(a.RefreshCurrentItem) a.ui.actionRefresh.OnTriggered(a.RefreshCurrentItem)
a.ui.dataInsertBtn.OnClicked(a.OnDataInsertClick)
a.ui.dataDelRowBtn.OnClicked(a.OnDataDeleteRowClick)
a.ui.dataCommitBtn.OnClicked(a.OnDataCommitClick)
// contentBox: set editable a.ui.actionAbout_Qt.OnTriggered(func() {
vcl_clear_grid(a.ui.contentBox) qt.QMessageBox_AboutQt2(a.ui.MainWindow.QWidget, APPNAME)
a.ui.contentBox.SetHorizontalScrollMode(qt.QAbstractItemView__ScrollPerPixel) })
a.ui.contentBox.SetVerticalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
a.ui.contentBox.OnCellChanged(a.OnDataCellEdited) a.contentTbl = NewTableState(a.ui.contentBox)
a.contentTbl.OnEdited = func() {
a.ui.actionApply_changes.SetEnabled(a.contentTbl.AnyChanges())
}
a.ui.actionDelete_row.OnTriggered(func() {
a.contentTbl.DeleteSelectedRows()
})
a.ui.actionAddRow.OnTriggered(func() {
a.contentTbl.InsertNewRow()
})
a.ui.actionApply_changes.OnTriggered(a.OnDataCommitClick)
// a.ui.queryInput.OnTextChanged(a.OnQueryTextChanged) // apply syntax highlighting // a.ui.queryInput.OnTextChanged(a.OnQueryTextChanged) // apply syntax highlighting
a.ui.queryExecBtn.OnClicked(a.OnQueryExecute) a.ui.mnuExecute.OnTriggered(a.OnQueryExecute)
if runtime.GOOS == "windows" { a.ui.queryInput.SetFont(vcl_monospace())
a.ui.queryInput.SetFontFamily("Consolas")
} else {
a.ui.queryInput.SetFontFamily("monospace")
}
vcl_clear_grid(a.ui.queryResult) a.resultsTbl = NewTableState(a.ui.queryResult)
a.ui.queryResult.SetHorizontalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
a.ui.queryResult.SetVerticalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
a.none = &noLoadedDatabase{} a.none = &noLoadedDatabase{}
a.OnNavChange(nil, nil) // calls f.none.RenderForNav and sets up status bar content a.refreshContent(nil) // calls f.none.RenderForNav and sets up status bar content
return a return a
} }
func main() { func main() {
qt.NewQApplication(os.Args) // Qt 6 already uses PassThrough as the default fractional scaling policy,
// no need to change anything here
sort.Slice(registeredDatabases, func(i, j int) bool { qt.NewQApplication(os.Args)
return registeredDatabases[i].label < registeredDatabases[j].label
}) // might not have been done yet
app := newApp() app := newApp()
app.ui.MainWindow.Show() app.ui.MainWindow.Show()
@@ -158,28 +136,15 @@ func (f *App) OnMnuHelpHomepage() {
} }
func (f *App) OnMenuHelpVersion() { func (f *App) OnMenuHelpVersion() {
connector := evLdbConnection{}
bi, ok := debug.ReadBuildInfo() ld, name, err := connector.Connect(context.Background())
if !ok { if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
return return
} }
info := "This version of " + APPNAME + " was compiled with:\n\n" f.addTopLevelDatabaseConnection(ld, name)
for _, dep := range bi.Deps {
// Filter to only interesting things
switch dep.Path {
case `github.com/cockroachdb/pebble`,
`github.com/dgraph-io/badger/v4`,
`github.com/mattn/go-sqlite3`,
`github.com/redis/go-redis/v9`,
`go.etcd.io/bbolt`,
`modernc.org/sqlite`:
info += fmt.Sprintf("- %s %s\n", dep.Path, dep.Version)
}
}
qt.QMessageBox_About(f.ui.MainWindow.QWidget, APPNAME, info)
} }
func (f *App) OnNavContextPopup(pos *qt.QPoint) { func (f *App) OnNavContextPopup(pos *qt.QPoint) {
@@ -247,18 +212,8 @@ func (f *App) RefreshCurrentItem() {
return // nothing to do return // nothing to do
} }
// If there were any edited rows, refreshing will lose them
// Prompt for confirmation first
// FIXME this should also trigger when changing selection on LHS nav??
if len(f.deleteRows) > 0 || len(f.updateRows) > 0 || len(f.deleteRows) > 0 {
ok := qt.QMessageBox_Warning2(f.ui.MainWindow.QWidget, APPNAME, "Your modifications have not been applied. Continue without applying?", qt.QMessageBox__Ok, qt.QMessageBox__Cancel)
if ok != int(qt.QMessageBox__Ok) {
return
}
}
f.OnNavContextRefresh(curItem) // Refresh LHS pane/children f.OnNavContextRefresh(curItem) // Refresh LHS pane/children
f.OnNavChange(curItem, curItem) // Refresh RHS pane/data content f.OnNavChange(curItem, curItem) // Refresh RHS pane/data content and warn if unsaved changes
} }
func (f *App) OnNavContextRefresh(item *qt.QTreeWidgetItem) { func (f *App) OnNavContextRefresh(item *qt.QTreeWidgetItem) {
@@ -283,90 +238,17 @@ func (f *App) OnNavContextRefresh(item *qt.QTreeWidgetItem) {
item.SetExpanded(isExpanded) item.SetExpanded(isExpanded)
} }
func (f *App) OnDataCellEdited(aRow, aCol int) {
if !f.isEditing {
return // current in full table refresh, this signal is not a true edit
}
// If this is an insert row, no need to patch updateRows
if _, ok := f.insertRows[aRow]; ok {
return // nothing to do
}
if chk, ok := f.updateRows[aRow]; ok {
if slice_contains(chk, aCol) {
// nothing to do
} else {
chk = append(chk, aCol)
f.updateRows[aRow] = chk
}
} else {
f.updateRows[aRow] = []int{aCol}
}
// If this row was marked for deletion, this new edit takes priority
delete(f.deleteRows, aRow)
// Recolour the row
explicitCol := qt.NewQBrush4(CO_EDIT_EXPLICIT)
implicitCol := qt.NewQBrush4(CO_EDIT_IMPLICIT)
numCols := f.ui.contentBox.ColumnCount()
for i := 0; i < numCols; i++ {
cell := f.ui.contentBox.Item(aRow, i)
if i == aCol {
cell.SetBackground(explicitCol)
} else {
cell.SetBackground(implicitCol) // FIXME preserve explicitCol for other changed cells too!
}
}
}
func (f *App) OnDataInsertClick() {
if !f.ui.contentBox.IsEnabled() {
return // Not an active data view
}
rpos := f.ui.contentBox.RowCount()
f.ui.contentBox.SetRowCount(rpos + 1)
f.insertRows[rpos] = struct{}{}
// Create the cells, so they're not nil when we try and set the bgcolor
colCt := f.ui.contentBox.ColumnCount()
for i := 0; i < colCt; i++ {
f.ui.contentBox.SetItem(rpos, i, qt.NewQTableWidgetItem())
}
// Repaint cells
vcl_grid_set_row_colour(f.ui.contentBox, rpos, qt.NewQBrush4(CO_INSERT))
// Scroll to bottom
f.ui.contentBox.ScrollToBottom()
}
func (f *App) OnDataDeleteRowClick() {
if !f.ui.contentBox.IsEnabled() {
return // Not an active data view
}
rpos := f.ui.contentBox.CurrentRow()
f.deleteRows[rpos] = struct{}{}
// If this row was marked for edit, this takes priority
delete(f.updateRows, rpos)
// Repaint cells
vcl_grid_set_row_colour(f.ui.contentBox, rpos, qt.NewQBrush4(CO_DELETE))
}
func (f *App) OnDataCommitClick() { func (f *App) OnDataCommitClick() {
if !f.ui.contentBox.IsEnabled() { if !f.ui.contentBox.IsEnabled() {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "Can't apply changes: editing is disabled")
return // Not an active data view return // Not an active data view
} }
if !f.contentTbl.allowEdit {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "Can't apply changes: database is not editable")
return // ??? Shouldn't be able to reach this point
}
node := f.ui.Buckets.CurrentItem() node := f.ui.Buckets.CurrentItem()
if node == nil { if node == nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "No database selected") qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "No database selected")
@@ -386,16 +268,19 @@ func (f *App) OnDataCommitClick() {
return return
} }
err = editableLd.ApplyChanges(f, bucketPath) err = editableLd.ApplyChanges(f.contentTbl, bucketPath)
if err != nil { if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error()) qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
// fallthrough to refresh
} }
// Refresh content // Refresh content
f.OnNavChange(node, node) // Refresh RHS pane/data content // This disables the 'apply changes' button again as there are no remaining edits
// Don't call onNavChange here because we don't want to warn about changes
f.refreshContent(node) // Refresh RHS pane/data content
// Preserve scroll position // Preserve scroll position
// TODO // (Happens automatically with QTableView)
} }
func (f *App) OnNavContextClose() { func (f *App) OnNavContextClose() {
@@ -486,57 +371,116 @@ func (f *App) OnQueryExecute() {
ld := f.getLoadedDatabaseFor(node) ld := f.getLoadedDatabaseFor(node)
// bucketPath is only used for some databases, not for others
bucketPath, err := f.getBucketPathFor(node)
if err != nil {
panic(err) // shouldn't happen
}
queryableLd, ok := ld.(queryableLoadedDatabase) queryableLd, ok := ld.(queryableLoadedDatabase)
if !ok { if !ok {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "Unsupported action for this database") qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "Unsupported action for this database")
return return
} }
err := queryableLd.ExecQuery(queryString, f.ui.queryResult) f.resultsTbl.Wipeout()
err = queryableLd.ExecQuery(queryString, bucketPath, f.resultsTbl)
if err != nil { if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error()) qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
return return
} }
} }
func (f *App) OnNavChange(node *qt.QTreeWidgetItem, _ *qt.QTreeWidgetItem) { func (f *App) OnNavChange(node *qt.QTreeWidgetItem, prev *qt.QTreeWidgetItem) {
// If there are unsaved changes, we may want to block this change / switch
// back to the previous selection
// Prompt for confirmation first
if f.contentTbl.AnyChanges() {
ok := qt.QMessageBox_Warning2(f.ui.MainWindow.QWidget, APPNAME, "Your changes have not been saved.", qt.QMessageBox__Ok, qt.QMessageBox__Discard)
if ok != int(qt.QMessageBox__Discard) {
// We want to stop this change
if prev == nil {
return // that's enough
}
// We need to block recursive triggering
// First time, to update , but doesn't update selection stat
f.ui.Buckets.BlockSignals(true)
f.ui.Buckets.SetCurrentItem(prev) // Also sets selection
f.ui.Buckets.BlockSignals(false)
f.ui.Buckets.SetEnabled(false)
// On next event loop tick:
mainthread.Start(func() {
// Second time
// Since it's already set, won't trigger onNavChange
f.ui.Buckets.SetCurrentItem(prev)
f.ui.Buckets.SetEnabled(true)
})
return
}
}
if node != prev {
// Changing tables (not just refreshing the current table)
// Reset scroll position to top
f.ui.contentBox.ScrollToTop()
}
// OK, continue with change
f.refreshContent(node)
}
func (f *App) refreshContent(node *qt.QTreeWidgetItem) {
var ld loadedDatabase = f.none var ld loadedDatabase = f.none
var bucketPath []string = nil var bucketPath []string = nil
if node != nil { if node != nil {
ld = f.getLoadedDatabaseFor(node) ld = f.getLoadedDatabaseFor(node)
bucketPath, _ = f.getBucketPathFor(node) // FIXME suppressing error var err error
bucketPath, err = f.getBucketPathFor(node)
if err != nil {
panic(err) // shouldn't happen
}
} }
// Reset some controls that the render function is expected to populate
f.insertRows = make(map[int]struct{})
f.updateRows = make(map[int][]int)
f.deleteRows = make(map[int]struct{})
f.isEditing = false
f.ui.propertiesBox.Clear() f.ui.propertiesBox.Clear()
vcl_clear_grid(f.ui.contentBox) propertiesText, err := ld.Properties(bucketPath)
err := ld.RenderForNav(f.ui, bucketPath) // Handover to the database type's own renderer function
if err != nil { if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error()) f.ui.propertiesBox.SetPlainText("Error loading properties: " + err.Error())
} else {
f.ui.propertiesBox.SetPlainText(propertiesText)
}
// Load database content
f.contentTbl.Wipeout()
err = ld.RenderForNav(f.contentTbl, bucketPath) // Handover to the database type's own renderer function
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, fmt.Sprintf("Loading contents of bucket %v: %s", bucketPath, err.Error()))
// Ensure elements are disabled // Ensure elements are disabled
f.ui.contentBox.SetEnabled(false) f.ui.contentBox.SetEnabled(false)
} }
// Toggle the Edit functionality // Toggle the Edit functionality
_, ok := ld.(editableLoadedDatabase) // Do this *after* RenderForNav as it disables editing by default.
f.ui.dataCommitBtn.SetEnabled(ok) _, editable := ld.(editableLoadedDatabase)
f.ui.dataDelRowBtn.SetEnabled(ok) editable = editable && f.contentTbl.IsReady() // if there was a failure loading, don't allow edit
f.ui.dataInsertBtn.SetEnabled(ok) f.ui.actionApply_changes.SetEnabled(false) // will be enabled after add/delete/edit
f.isEditing = ok f.ui.actionDelete_row.SetEnabled(editable)
f.ui.actionAddRow.SetEnabled(editable)
f.contentTbl.SetAllowEditing(editable)
// Toggle the Query functionality // Toggle the Query functionality
_, ok = ld.(queryableLoadedDatabase) _, queryable := ld.(queryableLoadedDatabase)
f.ui.queryInput.SetEnabled(ok) f.ui.queryInput.SetEnabled(queryable)
f.ui.queryResult.SetEnabled(ok) f.ui.queryResult.SetEnabled(queryable)
f.ui.queryExecBtn.SetEnabled(ok) f.ui.mnuExecute.SetEnabled(queryable)
// We're in charge of common status bar text updates // We're in charge of common status bar text updates
// Find database displayname // Find database displayname
@@ -561,7 +505,11 @@ func (f *App) DatabaseDisplayName(item *qt.QTreeWidgetItem) string {
return item.Text(0) return item.Text(0)
} }
func (f *App) OnNavExpanding(item *qt.QTreeWidgetItem) { func (f *App) handleNavExpansion(item *qt.QTreeWidgetItem, recurseDepth int) {
if item == nil {
panic("Expanding a nil item?")
}
if item.ChildCount() > 0 { if item.ChildCount() > 0 {
// We've already virtual-expanded this item once, don't repeat it // We've already virtual-expanded this item once, don't repeat it
@@ -590,21 +538,26 @@ func (f *App) OnNavExpanding(item *qt.QTreeWidgetItem) {
return return
} }
// While we're here - preload one single level deep (not any deeper) if recurseDepth > 0 {
// This makes it "seem" like we don't have empty virtual subfolders most of the time // While we're here - preload one single level deep (not any deeper)
// This makes it "seem" like we don't have empty virtual subfolders most of the time
childCt := item.ChildCount() childCt := item.ChildCount()
for i := 0; i < childCt; i++ { for i := 0; i < childCt; i++ {
cc := item.Child(i) cc := item.Child(i)
f.OnNavExpanding(cc) f.handleNavExpansion(cc, recurseDepth-1)
}
} }
}
func (f *App) OnNavExpanding(item *qt.QTreeWidgetItem) {
f.handleNavExpansion(item, 1)
} }
const ( const (
LoadedDatabaseIdRole = int(qt.UserRole + 100) LoadedDatabaseIdRole = int(qt.UserRole + 100)
BucketPathStringRole = int(qt.UserRole + 101) BucketPathBSliceRole = int(qt.UserRole + 101)
DataPrimaryKeyRole = int(qt.UserRole + 102) // sqlite DataPrimaryKeyRole = int(qt.UserRole + 102) // sqlite
) )
@@ -616,15 +569,9 @@ func (f *App) getLoadedDatabaseFor(item *qt.QTreeWidgetItem) loadedDatabase {
// getBucketPathFor gets the bucketPath array out of a child nav item. // getBucketPathFor gets the bucketPath array out of a child nav item.
func (f *App) getBucketPathFor(item *qt.QTreeWidgetItem) ([]string, error) { func (f *App) getBucketPathFor(item *qt.QTreeWidgetItem) ([]string, error) {
bucketPathJsonArr := item.Data(0, BucketPathStringRole).ToString() bucketPathJsonArr := item.Data(0, BucketPathBSliceRole).ToByteArray()
var ret []string return pathlist_decode(bucketPathJsonArr)
err := json.Unmarshal([]byte(bucketPathJsonArr), &ret)
if err != nil {
return nil, err
}
return ret, nil
} }
func (f *App) NavLoadChildren(item *qt.QTreeWidgetItem, bucketPath []string) error { func (f *App) NavLoadChildren(item *qt.QTreeWidgetItem, bucketPath []string) error {
@@ -652,10 +599,13 @@ func (f *App) NavLoadChildren(item *qt.QTreeWidgetItem, bucketPath []string) err
childPath = append(childPath, bucketPath...) childPath = append(childPath, bucketPath...)
childPath = append(childPath, bucketName) childPath = append(childPath, bucketName)
childPathJson, _ := json.Marshal(childPath) childPathJson, err := pathlist_encode(childPath)
if err != nil {
panic(err) // Shouldn't happen
}
node.SetData(0, LoadedDatabaseIdRole, qt.NewQVariant4(ldID)) node.SetData(0, LoadedDatabaseIdRole, qt.NewQVariant4(ldID))
node.SetData(0, BucketPathStringRole, qt.NewQVariant11(string(childPathJson))) node.SetData(0, BucketPathBSliceRole, qt.NewQVariant12(childPathJson))
item.AddChild(node) item.AddChild(node)
} }
@@ -675,10 +625,16 @@ func (f *App) addTopLevelDatabaseConnection(ld loadedDatabase, displayName strin
f.dbs_next++ f.dbs_next++
f.dbs[ldID] = ld f.dbs[ldID] = ld
emptyData, err := pathlist_encode([]string{}) // Initial browse position
if err != nil {
panic(err) // can't happen
}
nav.SetData(0, LoadedDatabaseIdRole, qt.NewQVariant4(ldID)) nav.SetData(0, LoadedDatabaseIdRole, qt.NewQVariant4(ldID))
nav.SetData(0, BucketPathStringRole, qt.NewQVariant11(`[]`)) // empty array nav.SetData(0, BucketPathBSliceRole, qt.NewQVariant12(emptyData))
f.ui.Buckets.AddTopLevelItem(nav) f.ui.Buckets.AddTopLevelItem(nav)
f.ui.Buckets.SetCurrentItem(nav) // Select new element f.ui.Buckets.SetCurrentItem(nav) // Select new element
f.handleNavExpansion(nav, 0) // Load child contents but do not recurse further
} }

View File

@@ -10,54 +10,43 @@ import (
) )
type MainWindowUi struct { type MainWindowUi struct {
MainWindow *qt.QMainWindow MainWindow *qt.QMainWindow
centralwidget *qt.QWidget centralwidget *qt.QWidget
gridLayout *qt.QGridLayout gridLayout *qt.QGridLayout
splitter *qt.QSplitter splitter *qt.QSplitter
Buckets *qt.QTreeWidget Buckets *qt.QTreeWidget
tabWidget *qt.QTabWidget tabWidget *qt.QTabWidget
tabProperties *qt.QWidget tabProperties *qt.QWidget
gridLayout_2 *qt.QGridLayout gridLayout_2 *qt.QGridLayout
propertiesBox *qt.QTextEdit propertiesBox *qt.QTextEdit
tabData *qt.QWidget tabData *qt.QWidget
verticalLayout *qt.QVBoxLayout verticalLayout *qt.QVBoxLayout
horizontalLayout *qt.QHBoxLayout contentBox *qt.QTableView
dataRefreshBtn *qt.QToolButton tabQuery *qt.QWidget
dataInsertBtn *qt.QToolButton verticalLayout_2 *qt.QVBoxLayout
dataDelRowBtn *qt.QToolButton splitter_2 *qt.QSplitter
dataCommitBtn *qt.QToolButton queryInput *qt.QPlainTextEdit
horizontalSpacer *qt.QSpacerItem queryResult *qt.QTableView
contentBox *qt.QTableWidget menubar *qt.QMenuBar
tabQuery *qt.QWidget menu_File *qt.QMenu
verticalLayout_2 *qt.QVBoxLayout menu_Query *qt.QMenu
horizontalLayout_2 *qt.QHBoxLayout menu_Help *qt.QMenu
queryExecBtn *qt.QToolButton menu_Data *qt.QMenu
horizontalSpacer_2 *qt.QSpacerItem menu_Tools *qt.QMenu
splitter_2 *qt.QSplitter statusbar *qt.QStatusBar
queryInput *qt.QTextEdit toolBar *qt.QToolBar
queryResult *qt.QTableWidget actionE_xit *qt.QAction
menubar *qt.QMenuBar mnuExecute *qt.QAction
menu_File *qt.QMenu mnuDriverVersions *qt.QAction
menu_Query *qt.QMenu mnuHelpAbout *qt.QAction
menu_Help *qt.QMenu actionConnect *qt.QAction
statusbar *qt.QStatusBar actionRefresh *qt.QAction
actionE_xit *qt.QAction actionAbout_Qt *qt.QAction
mnuBadgerOpen *qt.QAction actionAddRow *qt.QAction
mnuBadgerNewInMemory *qt.QAction actionDelete_row *qt.QAction
mnuBoltNew *qt.QAction actionApply_changes *qt.QAction
mnuBoltOpenDatabase *qt.QAction actionConnectionManager *qt.QAction
mnuBoltOpenDatabaseReadonly *qt.QAction actionCreate_Bolt_database_from_zip *qt.QAction
mnuDebconfOpen *qt.QAction
mnuPebbleOpenDatabase *qt.QAction
mnuPebbleNewInMemory *qt.QAction
mnuRedisConnect *qt.QAction
mnuSqliteOpenDatabase *qt.QAction
mnuSqliteNewInMemory *qt.QAction
mnuSqliteExperimentalDriver *qt.QAction
mnuExecute *qt.QAction
mnuDriverVersions *qt.QAction
mnuHelpAbout *qt.QAction
actionConnect *qt.QAction
} }
// NewMainWindowUi creates all Qt widget classes for MainWindow. // NewMainWindowUi creates all Qt widget classes for MainWindow.
@@ -75,95 +64,14 @@ func NewMainWindowUi() *MainWindowUi {
actionE_xit__objectName := qt.NewQAnyStringView3("actionE_xit") actionE_xit__objectName := qt.NewQAnyStringView3("actionE_xit")
ui.actionE_xit.SetObjectName(*actionE_xit__objectName) ui.actionE_xit.SetObjectName(*actionE_xit__objectName)
actionE_xit__objectName.Delete() // setter copied value actionE_xit__objectName.Delete() // setter copied value
ui.mnuBadgerOpen = qt.NewQAction() /* miqt-uic: no handler for QAction property 'menuRole' */
mnuBadgerOpen__objectName := qt.NewQAnyStringView3("mnuBadgerOpen")
ui.mnuBadgerOpen.SetObjectName(*mnuBadgerOpen__objectName)
mnuBadgerOpen__objectName.Delete() // setter copied value
icon1 := qt.NewQIcon()
icon1.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuBadgerOpen.SetIcon(icon1)
ui.mnuBadgerNewInMemory = qt.NewQAction()
mnuBadgerNewInMemory__objectName := qt.NewQAnyStringView3("mnuBadgerNewInMemory")
ui.mnuBadgerNewInMemory.SetObjectName(*mnuBadgerNewInMemory__objectName)
mnuBadgerNewInMemory__objectName.Delete() // setter copied value
icon2 := qt.NewQIcon()
icon2.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuBadgerNewInMemory.SetIcon(icon2)
ui.mnuBoltNew = qt.NewQAction()
mnuBoltNew__objectName := qt.NewQAnyStringView3("mnuBoltNew")
ui.mnuBoltNew.SetObjectName(*mnuBoltNew__objectName)
mnuBoltNew__objectName.Delete() // setter copied value
icon3 := qt.NewQIcon()
icon3.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuBoltNew.SetIcon(icon3)
ui.mnuBoltOpenDatabase = qt.NewQAction()
mnuBoltOpenDatabase__objectName := qt.NewQAnyStringView3("mnuBoltOpenDatabase")
ui.mnuBoltOpenDatabase.SetObjectName(*mnuBoltOpenDatabase__objectName)
mnuBoltOpenDatabase__objectName.Delete() // setter copied value
icon4 := qt.NewQIcon()
icon4.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuBoltOpenDatabase.SetIcon(icon4)
ui.mnuBoltOpenDatabaseReadonly = qt.NewQAction()
mnuBoltOpenDatabaseReadonly__objectName := qt.NewQAnyStringView3("mnuBoltOpenDatabaseReadonly")
ui.mnuBoltOpenDatabaseReadonly.SetObjectName(*mnuBoltOpenDatabaseReadonly__objectName)
mnuBoltOpenDatabaseReadonly__objectName.Delete() // setter copied value
icon5 := qt.NewQIcon()
icon5.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuBoltOpenDatabaseReadonly.SetIcon(icon5)
ui.mnuDebconfOpen = qt.NewQAction()
mnuDebconfOpen__objectName := qt.NewQAnyStringView3("mnuDebconfOpen")
ui.mnuDebconfOpen.SetObjectName(*mnuDebconfOpen__objectName)
mnuDebconfOpen__objectName.Delete() // setter copied value
icon6 := qt.NewQIcon()
icon6.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuDebconfOpen.SetIcon(icon6)
ui.mnuPebbleOpenDatabase = qt.NewQAction()
mnuPebbleOpenDatabase__objectName := qt.NewQAnyStringView3("mnuPebbleOpenDatabase")
ui.mnuPebbleOpenDatabase.SetObjectName(*mnuPebbleOpenDatabase__objectName)
mnuPebbleOpenDatabase__objectName.Delete() // setter copied value
icon7 := qt.NewQIcon()
icon7.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuPebbleOpenDatabase.SetIcon(icon7)
ui.mnuPebbleNewInMemory = qt.NewQAction()
mnuPebbleNewInMemory__objectName := qt.NewQAnyStringView3("mnuPebbleNewInMemory")
ui.mnuPebbleNewInMemory.SetObjectName(*mnuPebbleNewInMemory__objectName)
mnuPebbleNewInMemory__objectName.Delete() // setter copied value
icon8 := qt.NewQIcon()
icon8.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuPebbleNewInMemory.SetIcon(icon8)
ui.mnuRedisConnect = qt.NewQAction()
mnuRedisConnect__objectName := qt.NewQAnyStringView3("mnuRedisConnect")
ui.mnuRedisConnect.SetObjectName(*mnuRedisConnect__objectName)
mnuRedisConnect__objectName.Delete() // setter copied value
icon9 := qt.NewQIcon()
icon9.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuRedisConnect.SetIcon(icon9)
ui.mnuSqliteOpenDatabase = qt.NewQAction()
mnuSqliteOpenDatabase__objectName := qt.NewQAnyStringView3("mnuSqliteOpenDatabase")
ui.mnuSqliteOpenDatabase.SetObjectName(*mnuSqliteOpenDatabase__objectName)
mnuSqliteOpenDatabase__objectName.Delete() // setter copied value
icon10 := qt.NewQIcon()
icon10.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuSqliteOpenDatabase.SetIcon(icon10)
ui.mnuSqliteNewInMemory = qt.NewQAction()
mnuSqliteNewInMemory__objectName := qt.NewQAnyStringView3("mnuSqliteNewInMemory")
ui.mnuSqliteNewInMemory.SetObjectName(*mnuSqliteNewInMemory__objectName)
mnuSqliteNewInMemory__objectName.Delete() // setter copied value
icon11 := qt.NewQIcon()
icon11.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuSqliteNewInMemory.SetIcon(icon11)
ui.mnuSqliteExperimentalDriver = qt.NewQAction()
mnuSqliteExperimentalDriver__objectName := qt.NewQAnyStringView3("mnuSqliteExperimentalDriver")
ui.mnuSqliteExperimentalDriver.SetObjectName(*mnuSqliteExperimentalDriver__objectName)
mnuSqliteExperimentalDriver__objectName.Delete() // setter copied value
/* miqt-uic: no handler for QAction property 'checkable' */
ui.mnuExecute = qt.NewQAction() ui.mnuExecute = qt.NewQAction()
mnuExecute__objectName := qt.NewQAnyStringView3("mnuExecute") mnuExecute__objectName := qt.NewQAnyStringView3("mnuExecute")
ui.mnuExecute.SetObjectName(*mnuExecute__objectName) ui.mnuExecute.SetObjectName(*mnuExecute__objectName)
mnuExecute__objectName.Delete() // setter copied value mnuExecute__objectName.Delete() // setter copied value
icon12 := qt.NewQIcon() icon1 := qt.NewQIcon()
icon12.AddFile4(":/assets/resultset_next.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off) icon1.AddFile4(":/assets/resultset_next.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuExecute.SetIcon(icon12) ui.mnuExecute.SetIcon(icon1)
ui.mnuDriverVersions = qt.NewQAction() ui.mnuDriverVersions = qt.NewQAction()
mnuDriverVersions__objectName := qt.NewQAnyStringView3("mnuDriverVersions") mnuDriverVersions__objectName := qt.NewQAnyStringView3("mnuDriverVersions")
ui.mnuDriverVersions.SetObjectName(*mnuDriverVersions__objectName) ui.mnuDriverVersions.SetObjectName(*mnuDriverVersions__objectName)
@@ -172,13 +80,72 @@ func NewMainWindowUi() *MainWindowUi {
mnuHelpAbout__objectName := qt.NewQAnyStringView3("mnuHelpAbout") mnuHelpAbout__objectName := qt.NewQAnyStringView3("mnuHelpAbout")
ui.mnuHelpAbout.SetObjectName(*mnuHelpAbout__objectName) ui.mnuHelpAbout.SetObjectName(*mnuHelpAbout__objectName)
mnuHelpAbout__objectName.Delete() // setter copied value mnuHelpAbout__objectName.Delete() // setter copied value
icon2 := qt.NewQIcon()
icon2.AddFile4(":/assets/help.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.mnuHelpAbout.SetIcon(icon2)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionConnect = qt.NewQAction() ui.actionConnect = qt.NewQAction()
actionConnect__objectName := qt.NewQAnyStringView3("actionConnect") actionConnect__objectName := qt.NewQAnyStringView3("actionConnect")
ui.actionConnect.SetObjectName(*actionConnect__objectName) ui.actionConnect.SetObjectName(*actionConnect__objectName)
actionConnect__objectName.Delete() // setter copied value actionConnect__objectName.Delete() // setter copied value
icon13 := qt.NewQIcon() icon3 := qt.NewQIcon()
icon13.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off) icon3.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionConnect.SetIcon(icon13) ui.actionConnect.SetIcon(icon3)
ui.actionRefresh = qt.NewQAction()
actionRefresh__objectName := qt.NewQAnyStringView3("actionRefresh")
ui.actionRefresh.SetObjectName(*actionRefresh__objectName)
actionRefresh__objectName.Delete() // setter copied value
icon4 := qt.NewQIcon()
icon4.AddFile4(":/assets/arrow_refresh.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionRefresh.SetIcon(icon4)
/* miqt-uic: no handler for QAction property 'menuRole' */
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
icon5 := qt.NewQIcon()
icon5.AddFile4(":/assets/vendor_qt.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionAbout_Qt.SetIcon(icon5)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionAddRow = qt.NewQAction()
actionAddRow__objectName := qt.NewQAnyStringView3("actionAddRow")
ui.actionAddRow.SetObjectName(*actionAddRow__objectName)
actionAddRow__objectName.Delete() // setter copied value
icon6 := qt.NewQIcon()
icon6.AddFile4(":/assets/add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionAddRow.SetIcon(icon6)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionDelete_row = qt.NewQAction()
actionDelete_row__objectName := qt.NewQAnyStringView3("actionDelete_row")
ui.actionDelete_row.SetObjectName(*actionDelete_row__objectName)
actionDelete_row__objectName.Delete() // setter copied value
icon7 := qt.NewQIcon()
icon7.AddFile4(":/assets/delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionDelete_row.SetIcon(icon7)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionApply_changes = qt.NewQAction()
actionApply_changes__objectName := qt.NewQAnyStringView3("actionApply_changes")
ui.actionApply_changes.SetObjectName(*actionApply_changes__objectName)
actionApply_changes__objectName.Delete() // setter copied value
icon8 := qt.NewQIcon()
icon8.AddFile4(":/assets/pencil_go.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionApply_changes.SetIcon(icon8)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionConnectionManager = qt.NewQAction()
actionConnectionManager__objectName := qt.NewQAnyStringView3("actionConnectionManager")
ui.actionConnectionManager.SetObjectName(*actionConnectionManager__objectName)
actionConnectionManager__objectName.Delete() // setter copied value
icon9 := qt.NewQIcon()
icon9.AddFile4(":/assets/database_key.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionConnectionManager.SetIcon(icon9)
/* miqt-uic: no handler for QAction property 'menuRole' */
ui.actionCreate_Bolt_database_from_zip = qt.NewQAction()
actionCreate_Bolt_database_from_zip__objectName := qt.NewQAnyStringView3("actionCreate_Bolt_database_from_zip")
ui.actionCreate_Bolt_database_from_zip.SetObjectName(*actionCreate_Bolt_database_from_zip__objectName)
actionCreate_Bolt_database_from_zip__objectName.Delete() // setter copied value
icon10 := qt.NewQIcon()
icon10.AddFile4(":/assets/compress.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.actionCreate_Bolt_database_from_zip.SetIcon(icon10)
ui.centralwidget = qt.NewQWidget(ui.MainWindow.QWidget) ui.centralwidget = qt.NewQWidget(ui.MainWindow.QWidget)
centralwidget__objectName := qt.NewQAnyStringView3("centralwidget") centralwidget__objectName := qt.NewQAnyStringView3("centralwidget")
ui.centralwidget.SetObjectName(*centralwidget__objectName) ui.centralwidget.SetObjectName(*centralwidget__objectName)
@@ -234,9 +201,9 @@ func NewMainWindowUi() *MainWindowUi {
ui.propertiesBox.SetReadOnly(true) ui.propertiesBox.SetReadOnly(true)
ui.gridLayout_2.AddWidget2(ui.propertiesBox.QWidget, 0, 0) ui.gridLayout_2.AddWidget2(ui.propertiesBox.QWidget, 0, 0)
icon14 := qt.NewQIcon() icon11 := qt.NewQIcon()
icon14.AddFile4(":/assets/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off) icon11.AddFile4(":/assets/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabProperties, icon14, "") ui.tabWidget.AddTab2(ui.tabProperties, icon11, "")
ui.tabData = qt.NewQWidget(ui.tabWidget.QWidget) ui.tabData = qt.NewQWidget(ui.tabWidget.QWidget)
tabData__objectName := qt.NewQAnyStringView3("tabData") tabData__objectName := qt.NewQAnyStringView3("tabData")
ui.tabData.SetObjectName(*tabData__objectName) ui.tabData.SetObjectName(*tabData__objectName)
@@ -247,58 +214,7 @@ func NewMainWindowUi() *MainWindowUi {
verticalLayout__objectName.Delete() // setter copied value verticalLayout__objectName.Delete() // setter copied value
ui.verticalLayout.SetContentsMargins(11, 11, 11, 11) ui.verticalLayout.SetContentsMargins(11, 11, 11, 11)
ui.verticalLayout.SetSpacing(6) ui.verticalLayout.SetSpacing(6)
ui.horizontalLayout = qt.NewQHBoxLayout2() ui.contentBox = qt.NewQTableView(ui.tabData)
horizontalLayout__objectName := qt.NewQAnyStringView3("horizontalLayout")
ui.horizontalLayout.SetObjectName(*horizontalLayout__objectName)
horizontalLayout__objectName.Delete() // setter copied value
ui.horizontalLayout.SetContentsMargins(0, 0, 0, 0)
ui.horizontalLayout.SetSpacing(6)
ui.dataRefreshBtn = qt.NewQToolButton(ui.tabData)
dataRefreshBtn__objectName := qt.NewQAnyStringView3("dataRefreshBtn")
ui.dataRefreshBtn.SetObjectName(*dataRefreshBtn__objectName)
dataRefreshBtn__objectName.Delete() // setter copied value
icon15 := qt.NewQIcon()
icon15.AddFile4(":/assets/arrow_refresh.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.dataRefreshBtn.SetIcon(icon15)
ui.dataRefreshBtn.SetAutoRaise(true)
ui.horizontalLayout.AddWidget(ui.dataRefreshBtn.QWidget)
ui.dataInsertBtn = qt.NewQToolButton(ui.tabData)
dataInsertBtn__objectName := qt.NewQAnyStringView3("dataInsertBtn")
ui.dataInsertBtn.SetObjectName(*dataInsertBtn__objectName)
dataInsertBtn__objectName.Delete() // setter copied value
icon16 := qt.NewQIcon()
icon16.AddFile4(":/assets/add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.dataInsertBtn.SetIcon(icon16)
ui.dataInsertBtn.SetAutoRaise(true)
ui.horizontalLayout.AddWidget(ui.dataInsertBtn.QWidget)
ui.dataDelRowBtn = qt.NewQToolButton(ui.tabData)
dataDelRowBtn__objectName := qt.NewQAnyStringView3("dataDelRowBtn")
ui.dataDelRowBtn.SetObjectName(*dataDelRowBtn__objectName)
dataDelRowBtn__objectName.Delete() // setter copied value
icon17 := qt.NewQIcon()
icon17.AddFile4(":/assets/delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.dataDelRowBtn.SetIcon(icon17)
ui.dataDelRowBtn.SetAutoRaise(true)
ui.horizontalLayout.AddWidget(ui.dataDelRowBtn.QWidget)
ui.dataCommitBtn = qt.NewQToolButton(ui.tabData)
dataCommitBtn__objectName := qt.NewQAnyStringView3("dataCommitBtn")
ui.dataCommitBtn.SetObjectName(*dataCommitBtn__objectName)
dataCommitBtn__objectName.Delete() // setter copied value
icon18 := qt.NewQIcon()
icon18.AddFile4(":/assets/pencil_go.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.dataCommitBtn.SetIcon(icon18)
ui.dataCommitBtn.SetAutoRaise(true)
ui.horizontalLayout.AddWidget(ui.dataCommitBtn.QWidget)
ui.horizontalSpacer = qt.NewQSpacerItem4(40, 20, qt.QSizePolicy__Expanding, qt.QSizePolicy__Minimum)
ui.horizontalLayout.AddItem(ui.horizontalSpacer.QLayoutItem)
ui.verticalLayout.AddLayout(ui.horizontalLayout.QLayout)
ui.contentBox = qt.NewQTableWidget(ui.tabData)
contentBox__objectName := qt.NewQAnyStringView3("contentBox") contentBox__objectName := qt.NewQAnyStringView3("contentBox")
ui.contentBox.SetObjectName(*contentBox__objectName) ui.contentBox.SetObjectName(*contentBox__objectName)
contentBox__objectName.Delete() // setter copied value contentBox__objectName.Delete() // setter copied value
@@ -306,9 +222,9 @@ func NewMainWindowUi() *MainWindowUi {
ui.contentBox.SetHorizontalScrollMode(qt.QAbstractItemView__ScrollPerPixel) ui.contentBox.SetHorizontalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
ui.verticalLayout.AddWidget(ui.contentBox.QWidget) ui.verticalLayout.AddWidget(ui.contentBox.QWidget)
icon19 := qt.NewQIcon() icon12 := qt.NewQIcon()
icon19.AddFile4(":/assets/table.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off) icon12.AddFile4(":/assets/table.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabData, icon19, "") ui.tabWidget.AddTab2(ui.tabData, icon12, "")
ui.tabQuery = qt.NewQWidget(ui.tabWidget.QWidget) ui.tabQuery = qt.NewQWidget(ui.tabWidget.QWidget)
tabQuery__objectName := qt.NewQAnyStringView3("tabQuery") tabQuery__objectName := qt.NewQAnyStringView3("tabQuery")
ui.tabQuery.SetObjectName(*tabQuery__objectName) ui.tabQuery.SetObjectName(*tabQuery__objectName)
@@ -319,40 +235,19 @@ func NewMainWindowUi() *MainWindowUi {
verticalLayout_2__objectName.Delete() // setter copied value verticalLayout_2__objectName.Delete() // setter copied value
ui.verticalLayout_2.SetContentsMargins(11, 11, 11, 11) ui.verticalLayout_2.SetContentsMargins(11, 11, 11, 11)
ui.verticalLayout_2.SetSpacing(6) ui.verticalLayout_2.SetSpacing(6)
ui.horizontalLayout_2 = qt.NewQHBoxLayout2()
horizontalLayout_2__objectName := qt.NewQAnyStringView3("horizontalLayout_2")
ui.horizontalLayout_2.SetObjectName(*horizontalLayout_2__objectName)
horizontalLayout_2__objectName.Delete() // setter copied value
ui.horizontalLayout_2.SetContentsMargins(0, 0, 0, 0)
ui.horizontalLayout_2.SetSpacing(6)
ui.queryExecBtn = qt.NewQToolButton(ui.tabQuery)
queryExecBtn__objectName := qt.NewQAnyStringView3("queryExecBtn")
ui.queryExecBtn.SetObjectName(*queryExecBtn__objectName)
queryExecBtn__objectName.Delete() // setter copied value
icon20 := qt.NewQIcon()
icon20.AddFile4(":/assets/resultset_next.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.queryExecBtn.SetIcon(icon20)
ui.queryExecBtn.SetAutoRaise(true)
ui.horizontalLayout_2.AddWidget(ui.queryExecBtn.QWidget)
ui.horizontalSpacer_2 = qt.NewQSpacerItem4(40, 20, qt.QSizePolicy__Expanding, qt.QSizePolicy__Minimum)
ui.horizontalLayout_2.AddItem(ui.horizontalSpacer_2.QLayoutItem)
ui.verticalLayout_2.AddLayout(ui.horizontalLayout_2.QLayout)
ui.splitter_2 = qt.NewQSplitter(ui.tabQuery) ui.splitter_2 = qt.NewQSplitter(ui.tabQuery)
splitter_2__objectName := qt.NewQAnyStringView3("splitter_2") splitter_2__objectName := qt.NewQAnyStringView3("splitter_2")
ui.splitter_2.SetObjectName(*splitter_2__objectName) ui.splitter_2.SetObjectName(*splitter_2__objectName)
splitter_2__objectName.Delete() // setter copied value splitter_2__objectName.Delete() // setter copied value
ui.splitter_2.SetOrientation(qt.Vertical) ui.splitter_2.SetOrientation(qt.Vertical)
ui.splitter_2.SetChildrenCollapsible(false) ui.splitter_2.SetChildrenCollapsible(false)
ui.queryInput = qt.NewQTextEdit(ui.splitter_2.QWidget) ui.queryInput = qt.NewQPlainTextEdit(ui.splitter_2.QWidget)
queryInput__objectName := qt.NewQAnyStringView3("queryInput") queryInput__objectName := qt.NewQAnyStringView3("queryInput")
ui.queryInput.SetObjectName(*queryInput__objectName) ui.queryInput.SetObjectName(*queryInput__objectName)
queryInput__objectName.Delete() // setter copied value queryInput__objectName.Delete() // setter copied value
/* miqt-uic: no handler for queryInput property 'font' */ /* miqt-uic: no handler for queryInput property 'font' */
ui.splitter_2.AddWidget(ui.queryInput.QWidget) ui.splitter_2.AddWidget(ui.queryInput.QWidget)
ui.queryResult = qt.NewQTableWidget(ui.splitter_2.QWidget) ui.queryResult = qt.NewQTableView(ui.splitter_2.QWidget)
queryResult__objectName := qt.NewQAnyStringView3("queryResult") queryResult__objectName := qt.NewQAnyStringView3("queryResult")
ui.queryResult.SetObjectName(*queryResult__objectName) ui.queryResult.SetObjectName(*queryResult__objectName)
queryResult__objectName.Delete() // setter copied value queryResult__objectName.Delete() // setter copied value
@@ -361,9 +256,9 @@ func NewMainWindowUi() *MainWindowUi {
ui.splitter_2.AddWidget(ui.queryResult.QWidget) ui.splitter_2.AddWidget(ui.queryResult.QWidget)
ui.verticalLayout_2.AddWidget(ui.splitter_2.QWidget) ui.verticalLayout_2.AddWidget(ui.splitter_2.QWidget)
icon21 := qt.NewQIcon() icon13 := qt.NewQIcon()
icon21.AddFile4(":/assets/lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off) icon13.AddFile4(":/assets/lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.tabWidget.AddTab2(ui.tabQuery, icon21, "") ui.tabWidget.AddTab2(ui.tabQuery, icon13, "")
ui.splitter.AddWidget(ui.tabWidget.QWidget) ui.splitter.AddWidget(ui.tabWidget.QWidget)
ui.gridLayout.AddWidget2(ui.splitter.QWidget, 0, 0) ui.gridLayout.AddWidget2(ui.splitter.QWidget, 0, 0)
@@ -377,6 +272,7 @@ func NewMainWindowUi() *MainWindowUi {
menu_File__objectName := qt.NewQAnyStringView3("menu_File") menu_File__objectName := qt.NewQAnyStringView3("menu_File")
ui.menu_File.SetObjectName(*menu_File__objectName) ui.menu_File.SetObjectName(*menu_File__objectName)
menu_File__objectName.Delete() // setter copied value menu_File__objectName.Delete() // setter copied value
ui.menu_File.QWidget.AddAction(ui.actionConnectionManager)
ui.menu_File.QWidget.AddAction(ui.actionConnect) ui.menu_File.QWidget.AddAction(ui.actionConnect)
ui.menu_File.AddSeparator() ui.menu_File.AddSeparator()
ui.menu_File.QWidget.AddAction(ui.actionE_xit) ui.menu_File.QWidget.AddAction(ui.actionE_xit)
@@ -390,9 +286,27 @@ func NewMainWindowUi() *MainWindowUi {
ui.menu_Help.SetObjectName(*menu_Help__objectName) ui.menu_Help.SetObjectName(*menu_Help__objectName)
menu_Help__objectName.Delete() // setter copied value menu_Help__objectName.Delete() // setter copied value
ui.menu_Help.QWidget.AddAction(ui.mnuDriverVersions) ui.menu_Help.QWidget.AddAction(ui.mnuDriverVersions)
ui.menu_Help.AddSeparator()
ui.menu_Help.QWidget.AddAction(ui.actionAbout_Qt)
ui.menu_Help.QWidget.AddAction(ui.mnuHelpAbout) ui.menu_Help.QWidget.AddAction(ui.mnuHelpAbout)
ui.menu_Data = qt.NewQMenu(ui.menubar.QWidget)
menu_Data__objectName := qt.NewQAnyStringView3("menu_Data")
ui.menu_Data.SetObjectName(*menu_Data__objectName)
menu_Data__objectName.Delete() // setter copied value
ui.menu_Data.QWidget.AddAction(ui.actionRefresh)
ui.menu_Data.AddSeparator()
ui.menu_Data.QWidget.AddAction(ui.actionAddRow)
ui.menu_Data.QWidget.AddAction(ui.actionDelete_row)
ui.menu_Data.QWidget.AddAction(ui.actionApply_changes)
ui.menu_Tools = qt.NewQMenu(ui.menubar.QWidget)
menu_Tools__objectName := qt.NewQAnyStringView3("menu_Tools")
ui.menu_Tools.SetObjectName(*menu_Tools__objectName)
menu_Tools__objectName.Delete() // setter copied value
ui.menu_Tools.QWidget.AddAction(ui.actionCreate_Bolt_database_from_zip)
ui.menubar.AddMenu(ui.menu_File) ui.menubar.AddMenu(ui.menu_File)
ui.menubar.AddMenu(ui.menu_Data)
ui.menubar.AddMenu(ui.menu_Query) ui.menubar.AddMenu(ui.menu_Query)
ui.menubar.AddMenu(ui.menu_Tools)
ui.menubar.AddMenu(ui.menu_Help) ui.menubar.AddMenu(ui.menu_Help)
ui.MainWindow.SetMenuBar(ui.menubar) ui.MainWindow.SetMenuBar(ui.menubar)
ui.statusbar = qt.NewQStatusBar(ui.MainWindow.QWidget) ui.statusbar = qt.NewQStatusBar(ui.MainWindow.QWidget)
@@ -400,6 +314,23 @@ func NewMainWindowUi() *MainWindowUi {
ui.statusbar.SetObjectName(*statusbar__objectName) ui.statusbar.SetObjectName(*statusbar__objectName)
statusbar__objectName.Delete() // setter copied value statusbar__objectName.Delete() // setter copied value
ui.MainWindow.SetStatusBar(ui.statusbar) ui.MainWindow.SetStatusBar(ui.statusbar)
ui.toolBar = qt.NewQToolBar(ui.MainWindow.QWidget)
toolBar__objectName := qt.NewQAnyStringView3("toolBar")
ui.toolBar.SetObjectName(*toolBar__objectName)
toolBar__objectName.Delete() // setter copied value
/* miqt-uic: no handler for toolBar property 'iconSize' */
ui.toolBar.SetToolButtonStyle(qt.ToolButtonIconOnly)
ui.MainWindow.AddToolBar(qt.TopToolBarArea, ui.toolBar)
/* miqt-uic: no handler for toolBar attribute 'toolBarBreak' */
ui.toolBar.QWidget.AddAction(ui.actionConnectionManager)
ui.toolBar.QWidget.AddAction(ui.actionConnect)
ui.toolBar.AddSeparator()
ui.toolBar.QWidget.AddAction(ui.actionRefresh)
ui.toolBar.QWidget.AddAction(ui.actionAddRow)
ui.toolBar.QWidget.AddAction(ui.actionDelete_row)
ui.toolBar.QWidget.AddAction(ui.actionApply_changes)
ui.toolBar.AddSeparator()
ui.toolBar.QWidget.AddAction(ui.mnuExecute)
ui.Retranslate() ui.Retranslate()
@@ -410,37 +341,33 @@ func NewMainWindowUi() *MainWindowUi {
// Retranslate reapplies all text translations. // Retranslate reapplies all text translations.
func (ui *MainWindowUi) Retranslate() { func (ui *MainWindowUi) Retranslate() {
ui.MainWindow.SetWindowTitle(qt.QCoreApplication_Tr("yvbolt")) ui.MainWindow.SetWindowTitle(qt.QCoreApplication_Tr("QBolt"))
ui.actionE_xit.SetText(qt.QMainWindow_Tr("E&xit")) ui.actionE_xit.SetText(qt.QMainWindow_Tr("E&xit"))
ui.mnuBadgerOpen.SetText(qt.QMainWindow_Tr("Open database..."))
ui.mnuBadgerNewInMemory.SetText(qt.QMainWindow_Tr("New in-memory database"))
ui.mnuBoltNew.SetText(qt.QMainWindow_Tr("New database..."))
ui.mnuBoltOpenDatabase.SetText(qt.QMainWindow_Tr("Open database..."))
ui.mnuBoltOpenDatabaseReadonly.SetText(qt.QMainWindow_Tr("Open database (read-only)..."))
ui.mnuDebconfOpen.SetText(qt.QMainWindow_Tr("Open database..."))
ui.mnuPebbleOpenDatabase.SetText(qt.QMainWindow_Tr("Open database..."))
ui.mnuPebbleNewInMemory.SetText(qt.QMainWindow_Tr("New in-memory database"))
ui.mnuRedisConnect.SetText(qt.QMainWindow_Tr("Connect..."))
ui.mnuSqliteOpenDatabase.SetText(qt.QMainWindow_Tr("Open database..."))
ui.mnuSqliteNewInMemory.SetText(qt.QMainWindow_Tr("New in-memory database"))
ui.mnuSqliteExperimentalDriver.SetText(qt.QMainWindow_Tr("Connect using CLI driver (experimental)"))
ui.mnuExecute.SetText(qt.QMainWindow_Tr("E&xecute")) ui.mnuExecute.SetText(qt.QMainWindow_Tr("E&xecute"))
ui.mnuExecute.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F9"))) ui.mnuExecute.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F9")))
ui.mnuDriverVersions.SetText(qt.QMainWindow_Tr("Driver versions...")) ui.mnuDriverVersions.SetText(qt.QMainWindow_Tr("Driver versions..."))
ui.mnuHelpAbout.SetText(qt.QMainWindow_Tr("&About yvbolt")) ui.mnuHelpAbout.SetText(qt.QMainWindow_Tr("&About QBolt"))
ui.mnuHelpAbout.SetToolTip(qt.QMainWindow_Tr("About QBolt"))
ui.mnuHelpAbout.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F1"))) ui.mnuHelpAbout.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F1")))
ui.actionConnect.SetText(qt.QMainWindow_Tr("Connect...")) ui.actionConnect.SetText(qt.QMainWindow_Tr("Connect..."))
ui.actionConnect.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("Ctrl+O"))) ui.actionConnect.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("Ctrl+O")))
ui.actionRefresh.SetText(qt.QMainWindow_Tr("Refresh"))
ui.actionRefresh.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F5")))
ui.actionAbout_Qt.SetText(qt.QMainWindow_Tr("About Qt"))
ui.actionAddRow.SetText(qt.QMainWindow_Tr("Add row"))
ui.actionAddRow.SetToolTip(qt.QMainWindow_Tr("Add row"))
ui.actionDelete_row.SetText(qt.QMainWindow_Tr("Delete row"))
ui.actionApply_changes.SetText(qt.QMainWindow_Tr("Apply changes"))
ui.actionConnectionManager.SetText(qt.QMainWindow_Tr("Connection Manager"))
ui.actionConnectionManager.SetToolTip(qt.QMainWindow_Tr("Connection Manager"))
ui.actionCreate_Bolt_database_from_zip.SetText(qt.QMainWindow_Tr("Create Bolt database from zip"))
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabProperties), qt.QTabWidget_Tr("Properties")) ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabProperties), qt.QTabWidget_Tr("Properties"))
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabData), qt.QTabWidget_Tr("Data")) ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabData), qt.QTabWidget_Tr("Data"))
ui.dataRefreshBtn.SetToolTip(qt.QWidget_Tr("Refresh"))
ui.dataRefreshBtn.SetShortcut(qt.NewQKeySequence2(qt.QWidget_Tr("F5")))
ui.dataInsertBtn.SetToolTip(qt.QWidget_Tr("Insert"))
ui.dataDelRowBtn.SetToolTip(qt.QWidget_Tr("Delete Row"))
ui.dataCommitBtn.SetToolTip(qt.QWidget_Tr("Commit"))
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabQuery), qt.QTabWidget_Tr("Query")) ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabQuery), qt.QTabWidget_Tr("Query"))
ui.queryExecBtn.SetText(qt.QWidget_Tr("Execute"))
ui.menu_File.SetTitle(qt.QMenuBar_Tr("&File")) ui.menu_File.SetTitle(qt.QMenuBar_Tr("&File"))
ui.menu_Query.SetTitle(qt.QMenuBar_Tr("&Query")) ui.menu_Query.SetTitle(qt.QMenuBar_Tr("&Query"))
ui.menu_Help.SetTitle(qt.QMenuBar_Tr("&Help")) ui.menu_Help.SetTitle(qt.QMenuBar_Tr("&Help"))
ui.menu_Data.SetTitle(qt.QMenuBar_Tr("&Data"))
ui.menu_Tools.SetTitle(qt.QMenuBar_Tr("&Tools"))
ui.toolBar.SetWindowTitle(qt.QMainWindow_Tr("toolBar"))
} }

View File

@@ -11,7 +11,7 @@
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>yvbolt</string> <string>QBolt</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset resource="embed.qrc"> <iconset resource="embed.qrc">
@@ -84,83 +84,7 @@
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout"> <widget class="QTableView" name="contentBox">
<item>
<widget class="QToolButton" name="dataRefreshBtn">
<property name="toolTip">
<string>Refresh</string>
</property>
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/arrow_refresh.png</normaloff>:/assets/arrow_refresh.png</iconset>
</property>
<property name="shortcut">
<string>F5</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="dataInsertBtn">
<property name="toolTip">
<string>Insert</string>
</property>
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/add.png</normaloff>:/assets/add.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="dataDelRowBtn">
<property name="toolTip">
<string>Delete Row</string>
</property>
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/delete.png</normaloff>:/assets/delete.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="dataCommitBtn">
<property name="toolTip">
<string>Commit</string>
</property>
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/pencil_go.png</normaloff>:/assets/pencil_go.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="contentBox">
<property name="verticalScrollMode"> <property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum> <enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property> </property>
@@ -180,37 +104,6 @@
<string>Query</string> <string>Query</string>
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QToolButton" name="queryExecBtn">
<property name="text">
<string>Execute</string>
</property>
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/resultset_next.png</normaloff>:/assets/resultset_next.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item> <item>
<widget class="QSplitter" name="splitter_2"> <widget class="QSplitter" name="splitter_2">
<property name="orientation"> <property name="orientation">
@@ -219,14 +112,14 @@
<property name="childrenCollapsible"> <property name="childrenCollapsible">
<bool>false</bool> <bool>false</bool>
</property> </property>
<widget class="QTextEdit" name="queryInput"> <widget class="QPlainTextEdit" name="queryInput">
<property name="font"> <property name="font">
<font> <font>
<family>Monospace</family> <family>Monospace</family>
</font> </font>
</property> </property>
</widget> </widget>
<widget class="QTableWidget" name="queryResult"> <widget class="QTableView" name="queryResult">
<property name="verticalScrollMode"> <property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum> <enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property> </property>
@@ -256,6 +149,7 @@
<property name="title"> <property name="title">
<string>&amp;File</string> <string>&amp;File</string>
</property> </property>
<addaction name="actionConnectionManager"/>
<addaction name="actionConnect"/> <addaction name="actionConnect"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionE_xit"/> <addaction name="actionE_xit"/>
@@ -271,123 +165,68 @@
<string>&amp;Help</string> <string>&amp;Help</string>
</property> </property>
<addaction name="mnuDriverVersions"/> <addaction name="mnuDriverVersions"/>
<addaction name="separator"/>
<addaction name="actionAbout_Qt"/>
<addaction name="mnuHelpAbout"/> <addaction name="mnuHelpAbout"/>
</widget> </widget>
<widget class="QMenu" name="menu_Data">
<property name="title">
<string>&amp;Data</string>
</property>
<addaction name="actionRefresh"/>
<addaction name="separator"/>
<addaction name="actionAddRow"/>
<addaction name="actionDelete_row"/>
<addaction name="actionApply_changes"/>
</widget>
<widget class="QMenu" name="menu_Tools">
<property name="title">
<string>&amp;Tools</string>
</property>
<addaction name="actionCreate_Bolt_database_from_zip"/>
</widget>
<addaction name="menu_File"/> <addaction name="menu_File"/>
<addaction name="menu_Data"/>
<addaction name="menu_Query"/> <addaction name="menu_Query"/>
<addaction name="menu_Tools"/>
<addaction name="menu_Help"/> <addaction name="menu_Help"/>
</widget> </widget>
<widget class="QStatusBar" name="statusbar"/> <widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionConnectionManager"/>
<addaction name="actionConnect"/>
<addaction name="separator"/>
<addaction name="actionRefresh"/>
<addaction name="actionAddRow"/>
<addaction name="actionDelete_row"/>
<addaction name="actionApply_changes"/>
<addaction name="separator"/>
<addaction name="mnuExecute"/>
</widget>
<action name="actionE_xit"> <action name="actionE_xit">
<property name="text"> <property name="text">
<string>E&amp;xit</string> <string>E&amp;xit</string>
</property> </property>
</action> <property name="menuRole">
<action name="mnuBadgerOpen"> <enum>QAction::MenuRole::QuitRole</enum>
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>Open database...</string>
</property>
</action>
<action name="mnuBadgerNewInMemory">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>New in-memory database</string>
</property>
</action>
<action name="mnuBoltNew">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>New database...</string>
</property>
</action>
<action name="mnuBoltOpenDatabase">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>Open database...</string>
</property>
</action>
<action name="mnuBoltOpenDatabaseReadonly">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>Open database (read-only)...</string>
</property>
</action>
<action name="mnuDebconfOpen">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>Open database...</string>
</property>
</action>
<action name="mnuPebbleOpenDatabase">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>Open database...</string>
</property>
</action>
<action name="mnuPebbleNewInMemory">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>New in-memory database</string>
</property>
</action>
<action name="mnuRedisConnect">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>Connect...</string>
</property>
</action>
<action name="mnuSqliteOpenDatabase">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>Open database...</string>
</property>
</action>
<action name="mnuSqliteNewInMemory">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<property name="text">
<string>New in-memory database</string>
</property>
</action>
<action name="mnuSqliteExperimentalDriver">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Connect using CLI driver (experimental)</string>
</property> </property>
</action> </action>
<action name="mnuExecute"> <action name="mnuExecute">
@@ -408,12 +247,22 @@
</property> </property>
</action> </action>
<action name="mnuHelpAbout"> <action name="mnuHelpAbout">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/help.png</normaloff>:/assets/help.png</iconset>
</property>
<property name="text"> <property name="text">
<string>&amp;About yvbolt</string> <string>&amp;About QBolt</string>
</property>
<property name="toolTip">
<string>About QBolt</string>
</property> </property>
<property name="shortcut"> <property name="shortcut">
<string>F1</string> <string>F1</string>
</property> </property>
<property name="menuRole">
<enum>QAction::MenuRole::AboutRole</enum>
</property>
</action> </action>
<action name="actionConnect"> <action name="actionConnect">
<property name="icon"> <property name="icon">
@@ -427,6 +276,96 @@
<string>Ctrl+O</string> <string>Ctrl+O</string>
</property> </property>
</action> </action>
<action name="actionRefresh">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/arrow_refresh.png</normaloff>:/assets/arrow_refresh.png</iconset>
</property>
<property name="text">
<string>Refresh</string>
</property>
<property name="shortcut">
<string>F5</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionAbout_Qt">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/vendor_qt.png</normaloff>:/assets/vendor_qt.png</iconset>
</property>
<property name="text">
<string>About Qt</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::AboutQtRole</enum>
</property>
</action>
<action name="actionAddRow">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/add.png</normaloff>:/assets/add.png</iconset>
</property>
<property name="text">
<string>Add row</string>
</property>
<property name="toolTip">
<string>Add row</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionDelete_row">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/delete.png</normaloff>:/assets/delete.png</iconset>
</property>
<property name="text">
<string>Delete row</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionApply_changes">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/pencil_go.png</normaloff>:/assets/pencil_go.png</iconset>
</property>
<property name="text">
<string>Apply changes</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionConnectionManager">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_key.png</normaloff>:/assets/database_key.png</iconset>
</property>
<property name="text">
<string>Connection Manager</string>
</property>
<property name="toolTip">
<string>Connection Manager</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionCreate_Bolt_database_from_zip">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/compress.png</normaloff>:/assets/compress.png</iconset>
</property>
<property name="text">
<string>Create Bolt database from zip</string>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="embed.qrc"/> <include location="embed.qrc"/>

94
sqliteclidriver/event.go Normal file
View File

@@ -0,0 +1,94 @@
package sqliteclidriver
import (
"io"
"sync"
)
const (
evtypeStdout int = iota
evtypeStderr
evtypeExit
)
type processEvent struct {
evtype int
data []byte
err error
}
func (pe processEvent) Error() string {
if pe.err != nil {
return pe.err.Error()
}
if pe.evtype == evtypeStderr {
return string(pe.data)
}
return "<no error>"
}
func (pe processEvent) Unwrap() error {
return pe.err
}
//
func handleEvents(pw io.WriteCloser, pr, pe io.Reader, waiter func() error) (<-chan processEvent, io.WriteCloser, error) {
chEvents := make(chan processEvent, 0)
var wg sync.WaitGroup
go func() {
defer wg.Done()
processEventWorker(pr, evtypeStdout, chEvents)
}()
go func() {
defer wg.Done()
processEventWorker(pe, evtypeStderr, chEvents)
}()
wg.Add(2)
go func() {
// Only call cmd.Wait() after pipes are closed
wg.Wait()
err := waiter()
chEvents <- processEvent{
evtype: evtypeExit,
err: err,
}
close(chEvents)
}()
return chEvents, pw, nil
}
func processEventWorker(p io.Reader, evtype int, dest chan<- processEvent) {
for {
buf := make([]byte, 1024)
n, err := p.Read(buf)
if n > 0 {
dest <- processEvent{
evtype: evtype,
data: buf[0:n],
}
}
if err != nil {
dest <- processEvent{
evtype: evtype,
err: err,
}
// Assume all errors are permanent
// Ordering can produce either io.EOF, ErrClosedPipe, or PathError{"file already closed"}
return
}
}
}

View File

@@ -3,40 +3,15 @@ package sqliteclidriver
import ( import (
"io" "io"
"os/exec" "os/exec"
"sync"
) )
const ( func LocalBinEvents(databasePath string) (<-chan processEvent, io.WriteCloser, error) {
evtypeStdout int = iota // TODO find a way to strictly separate parameters and paths (`--` is not supported here)
evtypeStderr cmd := exec.Command(`/usr/bin/sqlite3`, `-noheader`, `-json`, databasePath)
evtypeExit return execEvents(cmd)
)
type processEvent struct {
evtype int
data []byte
err error
} }
func (pe processEvent) Error() string { func execEvents(cmd *exec.Cmd) (<-chan processEvent, io.WriteCloser, error) {
if pe.err != nil {
return pe.err.Error()
}
if pe.evtype == evtypeStderr {
return string(pe.data)
}
return "<no error>"
}
func (pe processEvent) Unwrap() error {
return pe.err
}
//
func ExecEvents(cmd *exec.Cmd) (<-chan processEvent, io.WriteCloser, error) {
pw, err := cmd.StdinPipe() pw, err := cmd.StdinPipe()
if err != nil { if err != nil {
@@ -58,58 +33,5 @@ func ExecEvents(cmd *exec.Cmd) (<-chan processEvent, io.WriteCloser, error) {
return nil, nil, err return nil, nil, err
} }
chEvents := make(chan processEvent, 0) return handleEvents(pw, pr, pe, cmd.Wait)
var wg sync.WaitGroup
go func() {
defer wg.Done()
processEventWorker(pr, evtypeStdout, chEvents)
}()
go func() {
defer wg.Done()
processEventWorker(pe, evtypeStderr, chEvents)
}()
wg.Add(2)
go func() {
// Only call cmd.Wait() after pipes are closed
wg.Wait()
err = cmd.Wait()
chEvents <- processEvent{
evtype: evtypeExit,
err: err,
}
close(chEvents)
}()
return chEvents, pw, nil
}
func processEventWorker(p io.Reader, evtype int, dest chan<- processEvent) {
for {
buf := make([]byte, 1024)
n, err := p.Read(buf)
if n > 0 {
dest <- processEvent{
evtype: evtype,
data: buf[0:n],
}
}
if err != nil {
dest <- processEvent{
evtype: evtype,
err: err,
}
// Assume all errors are permanent
// Ordering can produce either io.EOF, ErrClosedPipe, or PathError{"file already closed"}
return
}
}
} }

View File

@@ -13,7 +13,7 @@ import (
func TestEventCmd(t *testing.T) { func TestEventCmd(t *testing.T) {
cmd := exec.Command("/bin/bash", "-c", `echo "hello world"`) cmd := exec.Command("/bin/bash", "-c", `echo "hello world"`)
ch, _, err := ExecEvents(cmd) ch, _, err := execEvents(cmd)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -36,7 +36,7 @@ func TestEventCmd(t *testing.T) {
func TestEventCmdStdin(t *testing.T) { func TestEventCmdStdin(t *testing.T) {
cmd := exec.Command("/usr/bin/tr", "a-z", "A-Z") cmd := exec.Command("/usr/bin/tr", "a-z", "A-Z")
ch, pw, err := ExecEvents(cmd) ch, pw, err := execEvents(cmd)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -0,0 +1,94 @@
package sqliteclidriver
import (
"context"
"database/sql"
"database/sql/driver"
"fmt"
"io"
"strconv"
"golang.org/x/crypto/ssh"
)
func SSHEvents(cl *ssh.Client, databasePath string) (<-chan processEvent, io.WriteCloser, error) {
// Client must have already been Dial()'ed
ses, err := cl.NewSession()
if err != nil {
return nil, nil, fmt.Errorf("NewSession: %w", err)
}
pw, err := ses.StdinPipe()
if err != nil {
return nil, nil, fmt.Errorf("StdinPipe: %w", err)
}
pr, err := ses.StdoutPipe()
if err != nil {
return nil, nil, fmt.Errorf("StdoutPipe: %w", err)
}
pe, err := ses.StderrPipe()
if err != nil {
return nil, nil, fmt.Errorf("StderrPipe: %w", err)
}
// FIXME use accurate Bash escaping
// TODO find a way to strictly separate parameters and paths (`--` is not supported here)
ses.Start(`/usr/bin/sqlite3 -noheader -json "` + strconv.Quote(databasePath) + `"`)
return handleEvents(pw, pr, pe, ses.Wait)
}
//
type sshDriver struct {
cl *ssh.Client
}
func (d *sshDriver) Open(connectionString string) (driver.Conn, error) {
// TODO support custom binpath from our connection string
chEvents, pw, err := SSHEvents(d.cl, connectionString)
if err != nil {
return nil, err
}
return &SCConn{
listen: chEvents,
w: pw,
}, nil
}
var _ driver.Driver = &sshDriver{} // interface assertion
//
type sshConnector struct {
connectionString string
driver *sshDriver
}
func (c *sshConnector) Connect(context.Context) (driver.Conn, error) {
// FIXME support context
return c.driver.Open(c.connectionString)
}
func (c *sshConnector) Driver() driver.Driver {
return c.driver
}
var _ driver.Connector = &sshConnector{} // interface assertion
//
// OpenSSH opens an sql.DB connection over SSH using the supplied ssh.Client
// object.
func OpenSSH(cl *ssh.Client, databasePath string) *sql.DB {
return sql.OpenDB(&sshConnector{
connectionString: databasePath,
driver: &sshDriver{
cl: cl,
},
})
}

View File

@@ -21,25 +21,25 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os/exec"
"yvbolt/lexer" "qbolt/lexer"
"github.com/google/uuid" "github.com/google/uuid"
) )
const DriverName = `sqliteclidriver`
var ErrNotSupported = errors.New("Not supported") var ErrNotSupported = errors.New("Not supported")
// //
// SCDriver is a thing that can open connections to the database for a given
// connection string. It has no particular state.
type SCDriver struct{} type SCDriver struct{}
func (d *SCDriver) Open(connectionString string) (driver.Conn, error) { func (d *SCDriver) Open(connectionString string) (driver.Conn, error) {
// TODO support custom binpath from our connection string // TODO support custom binpath from our connection string
chEvents, pw, err := LocalBinEvents(connectionString)
cmd := exec.Command(`/usr/bin/sqlite3`, `-noheader`, `-json`, connectionString) // n.b. doesn't support `--`
chEvents, pw, err := ExecEvents(cmd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -53,17 +53,21 @@ func (d *SCDriver) Open(connectionString string) (driver.Conn, error) {
var _ driver.Driver = &SCDriver{} // interface assertion var _ driver.Driver = &SCDriver{} // interface assertion
func init() { func init() {
sql.Register("sqliteclidriver", &SCDriver{}) sql.Register(DriverName, &SCDriver{})
} }
// //
// SCConnector is a container holding a driver and connection string together.
// It is the result of parsing a sql.Open(driverName, connectionString) call
// and has not parsed anything.
type SCConnector struct { type SCConnector struct {
connectionString string connectionString string
driver *SCDriver driver *SCDriver
} }
func (c *SCConnector) Connect(context.Context) (driver.Conn, error) { func (c *SCConnector) Connect(context.Context) (driver.Conn, error) {
// FIXME support context
return c.driver.Open(c.connectionString) return c.driver.Open(c.connectionString)
} }
@@ -75,6 +79,7 @@ var _ driver.Connector = &SCConnector{} // interface assertion
// //
// SCConn represents a single, active, open, connection to the database.
type SCConn struct { type SCConn struct {
listen <-chan processEvent listen <-chan processEvent
w io.WriteCloser w io.WriteCloser
@@ -136,7 +141,7 @@ func (s *SCStmt) buildQuery(args []driver.Value) ([]byte, error) {
// Embed query params // Embed query params
// WARNING: Not secure against injection? That's not a security boundary // WARNING: Not secure against injection? That's not a security boundary
// for the purposes of yvbolt, but maybe for other package users // for the purposes of qbolt, but maybe for other package users
var querybuilder bytes.Buffer var querybuilder bytes.Buffer

View File

@@ -5,23 +5,65 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path/filepath"
"github.com/mappu/autoconfig" "github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6" qt "github.com/mappu/miqt/qt6"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
) )
type SSHTunnel struct { type SSHTunnel struct {
Server autoconfig.AddressPort Server autoconfig.AddressPort
Username string Username string
Password autoconfig.Password H1 autoconfig.Header `ylabel:"Authentication:" json:",omitempty"`
PrivateKeyContent autoconfig.MultiLineString Auth struct {
PrivateKeyFile autoconfig.ExistingFile Type autoconfig.OneOf
PrivateKeyPassword autoconfig.Password Password *struct {
Password autoconfig.Password
} `yicon:":/assets/key.png" json:",omitempty"`
PrivateKeyExternal *struct {
PrivateKeyFile autoconfig.ExistingFile `ylabel:"Private Key"`
PrivateKeyFilePassword autoconfig.Password `ylabel:"Password (optional)"`
} `ylabel:"Private key (External)" yicon:":/assets/page_key.png" json:",omitempty"`
PrivateKeyInternal *struct {
PrivateKeyContent autoconfig.MultiLineString `ylabel:"Private Key"`
PrivateKeyContentPassword autoconfig.Password `ylabel:"Password (optional)"`
} `ylabel:"Private key (Internal)" yicon:":/assets/page_key.png" json:",omitempty"`
SSHAgent *sshAgentConn `ylabel:"SSH Agent" yicon:":/assets/vendor_ssh.png" json:",omitempty"`
}
H2 autoconfig.Header `ylabel:"Server host key:" json:",omitempty"`
HostVerification struct {
Type autoconfig.OneOf
InsecureSkipVerify *struct{} `json:",omitempty"`
ExternalKnownHostsFile *struct {
Path autoconfig.ExistingFile
} `json:",omitempty"`
}
} }
func (s *SSHTunnel) InitDefaults() { // Used by autoconfig package when creating a new value func (s *SSHTunnel) Reset() { // Used by autoconfig package when creating a new value
s.Server.Port = 22 s.Server.Port = 22
if sshAuthSock := os.Getenv("SSH_AUTH_SOCK"); sshAuthSock != "" {
s.Auth.Type = "SSHAgent"
s.Auth.SSHAgent = &sshAgentConn{}
s.Auth.SSHAgent.Reset()
}
if homeDir, err := os.UserHomeDir(); err == nil {
defaultKnownHostsFile := filepath.Join(homeDir, `.ssh`, `known_hosts`)
if _, err := os.Stat(defaultKnownHostsFile); err == nil {
s.HostVerification.Type = "ExternalKnownHostsFile"
alloc_inplace(&s.HostVerification.ExternalKnownHostsFile)
s.HostVerification.ExternalKnownHostsFile.Path = autoconfig.ExistingFile(defaultKnownHostsFile)
}
}
if s.HostVerification.Type == "" {
s.HostVerification.Type = "InsecureSkipVerify"
alloc_inplace(&s.HostVerification.InsecureSkipVerify)
}
} }
func (s *SSHTunnel) String() string { func (s *SSHTunnel) String() string {
@@ -45,35 +87,69 @@ func (s SSHTunnel) Open(ctx context.Context) (*ssh.Client, error) {
return nil return nil
}, },
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // FIXME User: s.Username,
User: s.Username,
} }
if s.Password != "" { if s.Auth.Password != nil {
cfg.Auth = append(cfg.Auth, ssh.Password(string(s.Password))) cfg.Auth = append(cfg.Auth, ssh.Password(string(s.Auth.Password.Password)))
}
if s.PrivateKeyContent != "" { } else if s.Auth.PrivateKeyInternal != nil {
signer, err := parseMaybePasswordedKey([]byte(s.PrivateKeyContent), []byte(s.PrivateKeyPassword))
signer, err := parseMaybePasswordedKey(
[]byte(s.Auth.PrivateKeyInternal.PrivateKeyContent),
[]byte(s.Auth.PrivateKeyInternal.PrivateKeyContentPassword),
)
if err != nil { if err != nil {
return nil, fmt.Errorf("Parsing private key: %w", err) return nil, fmt.Errorf("Parsing private key: %w", err)
} }
cfg.Auth = append(cfg.Auth, ssh.PublicKeys(signer)) cfg.Auth = append(cfg.Auth, ssh.PublicKeys(signer))
}
if s.PrivateKeyFile != "" { } else if s.Auth.PrivateKeyExternal != nil {
keyBytes, err := os.ReadFile(string(s.PrivateKeyFile))
keyBytes, err := os.ReadFile(string(s.Auth.PrivateKeyExternal.PrivateKeyFile))
if err != nil { if err != nil {
return nil, fmt.Errorf("Loading private key: %w", err) return nil, fmt.Errorf("Loading private key: %w", err)
} }
signer, err := parseMaybePasswordedKey(keyBytes, []byte(s.PrivateKeyPassword)) signer, err := parseMaybePasswordedKey(keyBytes, []byte(s.Auth.PrivateKeyExternal.PrivateKeyFilePassword))
if err != nil { if err != nil {
return nil, fmt.Errorf("Parsing private key: %w", err) return nil, fmt.Errorf("Parsing private key: %w", err)
} }
cfg.Auth = append(cfg.Auth, ssh.PublicKeys(signer)) cfg.Auth = append(cfg.Auth, ssh.PublicKeys(signer))
} else if s.Auth.SSHAgent != nil {
sa, err := s.Auth.SSHAgent.getAgent()
if err != nil {
return nil, fmt.Errorf("Connecting to SSH agent: %w", err)
}
cfg.Auth = append(cfg.Auth, ssh.PublicKeysCallback(sa.Signers))
} else {
return nil, fmt.Errorf("Unsupported value %q for Auth", s.Auth.Type)
} }
//
if s.HostVerification.Type == "" || s.HostVerification.InsecureSkipVerify != nil {
cfg.HostKeyCallback = ssh.InsecureIgnoreHostKey()
} else if s.HostVerification.ExternalKnownHostsFile != nil {
cb, err := knownhosts.New(string(s.HostVerification.ExternalKnownHostsFile.Path))
if err != nil {
return nil, fmt.Errorf("Parsing known_hosts file %q: %w")
}
cfg.HostKeyCallback = cb
} else {
return nil, fmt.Errorf("Unsupported value %q for HostVerification", s.HostVerification.Type)
}
//
// TODO DialContext // TODO DialContext
// Use wrapper from https://github.com/golang/go/issues/20288 // Use wrapper from https://github.com/golang/go/issues/20288
return ssh.Dial("tcp", fmt.Sprintf("%s:%d", s.Server.Address, s.Server.Port), &cfg) return ssh.Dial("tcp", fmt.Sprintf("%s:%d", s.Server.Address, s.Server.Port), &cfg)

425
table.go Normal file
View File

@@ -0,0 +1,425 @@
package main
import (
qt "github.com/mappu/miqt/qt6"
)
type TableColumn interface {
SetRowCount(newlen int)
SetCell(aRow int, data any)
Display(aRow int) *qt.QVariant
Inspect(parent *qt.QWidget, aRow int)
ApplySimpleChange(aRow int, data *qt.QVariant)
SimpleEditData(aRow int) *qt.QVariant
IndicateBlue() bool
}
const (
CO_INSERT = qt.Yellow
CO_EDIT_IMPLICIT = qt.Green
CO_EDIT_EXPLICIT = qt.DarkGreen
CO_DELETE = qt.Red
CO_OBJECT = qt.Blue
)
type columnType int
const (
columnType_invalid columnType = iota
columnType_inlineText
columnType_bsonDoc
columnType_popupData
// TODO add a column type for map[string]string that can show content in a mini popup?? (Useful for redis + secretsvc)
)
type tableState struct {
rowCount int
columns []TableColumn
columnLabels []string
primaryKeys [][]byte
allowEdit bool
updateRows map[int][]int // row => []column
insertRows map[int]struct{}
deleteRows map[int]struct{}
OnEdited func()
model *qt.QAbstractTableModel
tbl *qt.QTableView
dg_normal, dg_bson, dg_binary *qt.QAbstractItemDelegate
}
// SetupColumns resets the table's content.
// It preserves the OnEdited callback
func (ts *tableState) SetupColumns(columns []columnType, labels []string) {
if ts.tbl.IsEnabled() {
ts.tbl.SetEnabled(false)
ts.model.BeginResetModel()
}
ts.columns = make([]TableColumn, len(columns))
ts.columnLabels = labels
ts.SetAllowEditing(false)
ts.updateRows = make(map[int][]int, 0)
ts.insertRows = make(map[int]struct{}, 0)
ts.deleteRows = make(map[int]struct{}, 0)
ts.primaryKeys = nil
for aCol, ctype := range columns {
switch ctype {
case columnType_inlineText:
ts.columns[aCol] = &stringColumn{}
// Reset renderer to something like the default Qt one
// Persist the delegate for the lifetime of this tableState - the
// QTableView does not take ownership of a supplied delegate(??)
// TODO is that true? Then why are we passing in the parent?
if ts.dg_normal == nil {
ts.dg_normal = qt.NewQStyledItemDelegate2(ts.tbl.QObject).QAbstractItemDelegate
}
ts.tbl.SetItemDelegateForColumn(aCol, ts.dg_normal)
case columnType_popupData:
ts.columns[aCol] = &binColumn{}
if ts.dg_binary == nil {
ts.dg_binary = table_makeBinaryItemDelegate(ts, ts.tbl.QObject)
}
ts.tbl.SetItemDelegateForColumn(aCol, ts.dg_binary)
case columnType_bsonDoc:
ts.columns[aCol] = &bsonColumn{}
if ts.dg_bson == nil {
ts.dg_bson = table_makeBsonItemDelegate(ts, ts.tbl.QObject)
}
ts.tbl.SetItemDelegateForColumn(aCol, ts.dg_bson)
default:
panic("bad columnType")
}
}
ts.SetRowCount(0) // populate slices
}
func (ts *tableState) SetAllowEditing(allow bool) {
ts.allowEdit = allow
if allow {
ts.tbl.SetEditTriggers(qt.QAbstractItemView__DoubleClicked | qt.QAbstractItemView__EditKeyPressed)
} else {
ts.tbl.SetEditTriggers(qt.QAbstractItemView__NoEditTriggers)
}
}
func (ts *tableState) AnyChanges() bool {
return len(ts.insertRows) > 0 || len(ts.updateRows) > 0 || len(ts.deleteRows) > 0
}
// AddRow inserts a new row.
// It's assumed to be initial data. For user-modified data, use InsertNewRow.
func (ts *tableState) AddRow() int {
newRowId := ts.rowCount
ts.SetRowCount(ts.rowCount + 1)
return newRowId
}
// AddRowData inserts a new row.
// It's assumed to be initial data. For user-modified data, use InsertNewRow.
func (ts *tableState) AddRowData(cells ...any) {
aRow := ts.AddRow()
for aCol, cellData := range cells {
ts.SetCell(aRow, aCol, cellData)
}
}
func (ts *tableState) AddRow_PK_Data(pk []byte, cells ...any) {
aRow := ts.AddRow()
ts.SetRowPrimaryKey(aRow, pk)
for aCol, cellData := range cells {
ts.SetCell(aRow, aCol, cellData)
}
}
func (ts *tableState) SetRowCount(newlen int) {
if ts.rowCount == newlen {
return // no change
}
ts.rowCount = newlen
for _, colIfac := range ts.columns {
colIfac.SetRowCount(newlen)
}
// We don't know if editing will be enabled until after the whole RenderForNav
// has completed. Set up primary key support regardless.
ts.primaryKeys = slice_set_len(ts.primaryKeys, newlen)
}
func (ts *tableState) SetCell(aRow, aCol int, data any) {
if aRow >= ts.rowCount {
panic("SetCell with too large row")
}
ts.columns[aCol].SetCell(aRow, data)
}
// SetRowPrimaryKey
// The slice data will be copied.
func (ts *tableState) SetRowPrimaryKey(aRow int, data []byte) {
if len(ts.primaryKeys) != ts.rowCount {
panic("messed up")
}
if aRow >= ts.rowCount {
panic("SetRowPrimaryKey with too large row")
}
ts.primaryKeys[aRow] = slice_dup(data)
}
func (ts *tableState) Wipeout() {
ts.SetupColumns(
[]columnType{
columnType_inlineText,
columnType_popupData,
},
[]string{"Key", "Value"},
)
}
func (ts *tableState) Ready() {
ts.model.EndResetModel()
ts.tbl.ResizeColumnsToContents()
ts.tbl.SetEnabled(true)
}
func (ts *tableState) IsReady() bool {
return ts.tbl.IsEnabled()
}
// DeleteSelectedRows marks the selected rows for deletion.
func (ts *tableState) DeleteSelectedRows() {
rows := ts.tbl.SelectionModel().SelectedRows()
// n.b. if the selection model for the table allows single-cell selection,
// this won't find all rows, just fully-selected rows from clicking on the
// vertical header bar
for _, row := range rows {
row_ := row.Row() // your boat
// Mark for deletion
ts.deleteRows[row_] = struct{}{}
// This supersedes any updates in the row
if _, ok := ts.updateRows[row_]; ok {
delete(ts.updateRows, row_)
}
// Refresh background colour
ts.model.DataChanged(&row, &row)
}
if len(rows) > 0 {
ts.OnEdited()
}
}
// InsertNewRow is the user action of inserting a new row at the bottom of the
// table.
func (ts *tableState) InsertNewRow() {
rowId := ts.rowCount
ts.model.BeginInsertRows(qt.NewQModelIndex(), rowId, rowId)
ts.AddRow()
ts.insertRows[rowId] = struct{}{}
ts.model.EndInsertRows()
// Scroll to bottom (only works after EndInsertRows has caused the view to
// refresh)
ts.tbl.ScrollToBottom()
// There are currently some edits again
ts.OnEdited()
}
func NewTableState(tbl *qt.QTableView) *tableState {
// Stylistic changes
tbl.SetHorizontalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
tbl.SetVerticalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
tbl.VerticalHeader().Hide() // Table row IDs do not match data IDs
tbl.SetSelectionBehavior(qt.QAbstractItemView__SelectRows)
// @ref https://doc.qt.io/qt-6/qabstracttablemodel.html#subclassing
model := qt.NewQAbstractTableModel2(tbl.QObject)
var ts tableState
ts.model = model
ts.tbl = tbl
ts.OnEdited = func() {} // no-op
ts.Wipeout()
model.OnRowCount(func(parent *qt.QModelIndex) int {
return ts.rowCount
})
model.OnColumnCount(func(parent *qt.QModelIndex) int {
return len(ts.columns)
})
model.OnHeaderData(func(super func(section int, orientation qt.Orientation, role int) *qt.QVariant, section int, orientation qt.Orientation, role int) *qt.QVariant {
if role == int(qt.DisplayRole) && orientation == qt.Horizontal {
if section >= len(ts.columnLabels) {
return qt.NewQVariant() // invalid request
}
return qt.NewQVariant11(ts.columnLabels[section])
}
return super(section, orientation, role)
})
model.OnFlags(func(super func(index *qt.QModelIndex) qt.ItemFlag, index *qt.QModelIndex) qt.ItemFlag {
ret := super(index)
if ts.allowEdit {
ret |= qt.ItemIsEditable
}
return ret
})
model.OnData(func(index *qt.QModelIndex, role int) *qt.QVariant {
if !index.IsValid() {
return qt.NewQVariant() // invalid
}
aCol := index.Column()
aRow := index.Row()
switch role {
case int(qt.DisplayRole):
// Formatting for display, return a QVariant of plain string type
return ts.columns[aCol].Display(aRow)
case int(qt.ForegroundRole):
if ts.columns[aCol].IndicateBlue() {
return qt.NewQColor2(CO_OBJECT).ToQVariant()
}
return qt.NewQVariant() // just default
case int(qt.EditRole):
// Supplying to the editor, return a QVariant of raw editing type
return ts.columns[aCol].SimpleEditData(aRow)
case int(qt.BackgroundRole):
if _, ok := ts.deleteRows[aRow]; ok {
return qt.NewQColor2(CO_DELETE).ToQVariant()
}
if _, ok := ts.insertRows[aRow]; ok {
return qt.NewQColor2(CO_INSERT).ToQVariant()
}
if cells, ok := ts.updateRows[aRow]; ok {
if slice_contains(cells, aCol) {
return qt.NewQColor2(CO_EDIT_EXPLICIT).ToQVariant()
} else {
return qt.NewQColor2(CO_EDIT_IMPLICIT).ToQVariant()
}
}
// Row is not modified
return qt.NewQVariant() // trigger default behaviour
default:
return qt.NewQVariant() // invalid
}
})
model.OnSetData(func(super func(index *qt.QModelIndex, value *qt.QVariant, role int) bool, index *qt.QModelIndex, value *qt.QVariant, role int) bool {
if !index.IsValid() {
return super(index, value, role) // invalid
}
aCol, aRow := index.Column(), index.Row()
switch role {
case int(qt.EditRole):
// Update content
ts.columns[aCol].ApplySimpleChange(aRow, value)
// If this is an insert row, no need to patch updateRows
if _, ok := ts.insertRows[aRow]; ok {
// nothing to do
} else {
// If this row was marked for deletion, this new edit takes priority
delete(ts.deleteRows, aRow)
// Set background colour
cells, ok := ts.updateRows[aRow]
if !ok {
cells = make([]int, 0, 1)
}
if slice_contains(cells, aCol) {
// already ok
} else {
cells = append(cells, aCol)
}
ts.updateRows[aRow] = cells
}
// Emit signal
// TODO ideally this would run after this function returned?
ts.OnEdited()
// Done
return true
default:
return super(index, value, role)
}
})
// Table cell double-click
tbl.OnDoubleClicked(func(idx *qt.QModelIndex) {
if !idx.IsValid() {
return
}
aCol, aRow := idx.Column(), idx.Row()
if !ts.allowEdit {
// If editing is enabled, double-clicking a cell should have been
// one of the edit-triggers
// So only show the custom inspector if editing is disabled
//
// Most column-types do not define an inspector and this is a no-op anyway
ts.columns[aCol].Inspect(tbl.QWidget, aRow)
}
})
// Set target QTableView's model
tbl.SetModel(model.QAbstractItemModel)
return &ts
}

176
table_binary.go Normal file
View File

@@ -0,0 +1,176 @@
package main
import (
"bytes"
"fmt"
"unicode/utf8"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
)
type binColumn struct {
vals [][]byte
}
func (bc *binColumn) SetRowCount(newlen int) {
bc.vals = slice_set_len(bc.vals, newlen)
}
func (bc *binColumn) SetCell(aRow int, data any) {
str, ok := data.([]byte)
if !ok {
panic("bad data type")
}
bc.vals[aRow] = str
}
func (bc *binColumn) Display(aRow int) *qt.QVariant {
return qt.NewQVariant11(formatUtf8(bc.vals[aRow]))
}
func (bc *binColumn) Inspect(parent *qt.QWidget, aRow int) {
// do nothing
// TODO support hexview for noneditable columns
}
func (bc *binColumn) SimpleEditData(aRow int) *qt.QVariant {
// Theoretically don't need to return anything here for simple-edits since
// the popup editor works directly with the column-store
return qt.NewQVariant12(bc.vals[aRow])
}
func (bc *binColumn) ApplySimpleChange(aRow int, data *qt.QVariant) {
// Do nothing - the editor delegate has already directly updated
// the value directly in the column store
// (And - somehow value.ToByteArray() has the "Editing..." string)
}
func (bc *binColumn) IndicateBlue() bool {
return false
}
var _ TableColumn = &binColumn{}
func table_makeBinaryItemDelegate(ts *tableState, parent *qt.QObject) *qt.QAbstractItemDelegate {
const (
ActionMode__invalid int = iota
ActionMode_Text_Readonly // Always a popup, otherwise you would just look at it
ActionMode_Text_Editable_Popup
//ActionMode_Text_Editable_Inline // TODO
ActionMode_Hex_Readonly
)
const ActionModeProperty string = `x-am`
zombie := qt.NewQStyledItemDelegate2(parent)
zombie.OnCreateEditor(func(super func(parent *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) *qt.QWidget, parent *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) *qt.QWidget {
// return a custom editor widget
aCol, aRow := index.Column(), index.Row()
self := ts.columns[aCol].(*binColumn)
bslice := self.vals[aRow]
// Check if this contains valid UTF-8 data.
// If so, show in a multiline text editor + allow editing if this tableState allowed editing
// If not, use the hexview popup
isText := utf8.Valid(bslice)
// The widget here is rendered stacked on top of the cell's existing DisplayRole
// It doesn't replace it
editor := qt.NewQLabel(parent)
editor.SetAutoFillBackground(true) // Hide parent
if isText && !ts.allowEdit {
editor.SetProperty(ActionModeProperty, qt.NewQVariant4(ActionMode_Text_Readonly))
editor.SetText("Viewing...")
// TODO
} else if isText && ts.allowEdit {
editor.SetProperty(ActionModeProperty, qt.NewQVariant4(ActionMode_Text_Editable_Popup))
editor.SetText("Editing...")
var textEdit autoconfig.MultiLineString
textEdit = autoconfig.MultiLineString(string(bslice))
autoconfig.OpenDialog(&textEdit, editor.QWidget, APPNAME, func() {
// Store change directly back into column store
self.vals[aRow] = []byte(string(textEdit))
// Signal success/finished editing
zombie.CommitData(editor.QWidget)
zombie.CloseEditor2(editor.QWidget, qt.QAbstractItemDelegate__NoHint)
})
} else {
// Binary data
// Popup hexview
editor.SetProperty(ActionModeProperty, qt.NewQVariant4(ActionMode_Hex_Readonly))
editor.SetText("Viewing...")
dlg := qt.NewQDialog(parent)
dlg.SetModal(true)
dlg.SetWindowTitle(fmt.Sprintf("%s - Hex view (%d bytes)", APPNAME, len(bslice)))
dlg.SetContentsMargins(0, 0, 0, 0)
layout := qt.NewQGridLayout(dlg.QWidget)
layout.SetContentsMargins(0, 0, 0, 0)
dlg.SetLayout(layout.QLayout)
hxvw := vcl_hexview(nil, bytes.NewReader(bslice), int64(len(bslice)))
layout.AddWidget2(hxvw, 0, 0)
// Dialog should be, by default, big enough to show the whole table at once
// Slightly overshoots on Breeze theme with default fonts
// TODO figure out the real width accurately
dlg.SetMinimumWidth(700)
dlg.Show()
dlg.OnFinished(func(_ int) {
// close editor, without, signalling commitData (this was readonly)
//zombie.CommitData(editor.QWidget)
zombie.CloseEditor2(editor.QWidget, qt.QAbstractItemDelegate__NoHint)
})
}
return editor.QWidget
})
zombie.OnSetEditorData(func(super func(editor *qt.QWidget, index *qt.QModelIndex), editor *qt.QWidget, index *qt.QModelIndex) {
actionType := editor.Property("x-am").ToInt()
switch actionType {
case ActionMode_Text_Readonly:
// TODO
case ActionMode_Text_Editable_Popup:
super(editor, index) // Save
case ActionMode_Hex_Readonly:
// do nothing
default:
panic("bad actionMode in editor delegate widget")
}
})
zombie.OnSetModelData(func(super func(editor *qt.QWidget, model *qt.QAbstractItemModel, index *qt.QModelIndex), editor *qt.QWidget, model *qt.QAbstractItemModel, index *qt.QModelIndex) {
actionType := editor.Property("x-am").ToInt()
switch actionType {
case ActionMode_Text_Readonly:
// TODO
case ActionMode_Text_Editable_Popup:
super(editor, model, index) // Save
case ActionMode_Hex_Readonly:
// do nothing
default:
panic("bad actionMode in editor delegate widget")
}
})
return zombie.QAbstractItemDelegate
}

142
table_bson.go Normal file
View File

@@ -0,0 +1,142 @@
package main
import (
"bytes"
"fmt"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"go.mongodb.org/mongo-driver/v2/bson"
)
type bsonColumn struct {
vals []bson.D
}
func (bc *bsonColumn) SetRowCount(newlen int) {
bc.vals = slice_set_len(bc.vals, newlen)
}
func (bc *bsonColumn) SetCell(aRow int, data any) {
obj, ok := data.(bson.D)
if !ok {
panic("bad data type")
}
bc.vals[aRow] = obj
}
func (bc *bsonColumn) Display(aRow int) *qt.QVariant {
return qt.NewQVariant11(fmt.Sprintf("BSON (%d keys)", len(bc.vals[aRow])))
}
func (bc *bsonColumn) Inspect(parent *qt.QWidget, aRow int) {
// Double-click on BSON column
// We aren't loading the editor
data := bc.vals[aRow] // shallow copy
// Deep-copy so that we can ensure no modification takes place
jb_before, err := data.MarshalJSON()
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, err.Error())
return // not reachable
}
autoconfig.OpenDialog(&data, parent, APPNAME, func() {
jb_after, err := data.MarshalJSON()
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, err.Error())
return // not reachable
}
if !bytes.Equal(jb_before, jb_after) {
// Show warning message
qt.QMessageBox_Warning(parent, APPNAME, "The data is read-only. Your changes will not be saved.")
// Restore data from previous JSON
err = data.UnmarshalJSON(jb_before)
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, err.Error())
return // not reachable
}
bc.vals[aRow] = data
}
})
}
func (bc *bsonColumn) SimpleEditData(aRow int) *qt.QVariant {
// Don't need to return anything here for simple-edits since
// the popup editor works directly with the column-store
return qt.NewQVariant()
}
func (bc *bsonColumn) ApplySimpleChange(aRow int, data *qt.QVariant) {
// Do nothing - the editor delegate has already directly updated
// the value directly in the column store
}
func (bc *bsonColumn) IndicateBlue() bool {
return true
}
var _ TableColumn = &bsonColumn{}
func table_makeBsonItemDelegate(ts *tableState, parent *qt.QObject) *qt.QAbstractItemDelegate {
zombie := qt.NewQStyledItemDelegate2(parent)
zombie.OnCreateEditor(func(super func(parent *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) *qt.QWidget, parent *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) *qt.QWidget {
// return a custom editor widget
aCol, aRow := index.Column(), index.Row()
self := ts.columns[aCol].(*bsonColumn)
ptr_bData := &self.vals[aRow]
// The widget here is rendered stacked on top of the cell's existing DisplayRole
// It doesn't replace it
foo := qt.NewQLabel(parent)
foo.SetAutoFillBackground(true) // Hide parent
foo.SetText("Editing...")
autoconfig.OpenDialog(ptr_bData, foo.QWidget, APPNAME, func() {
// onFinished
// We're already working with the data in-pointer, nothing to do here
// However - signal to the model that it's finished,
zombie.CommitData(foo.QWidget)
zombie.CloseEditor2(foo.QWidget, qt.QAbstractItemDelegate__NoHint)
})
return foo.QWidget
// return the default editor widget
//return super(parent, option, index)
})
zombie.OnSetEditorData(func(super func(editor *qt.QWidget, index *qt.QModelIndex), editor *qt.QWidget, index *qt.QModelIndex) {
// populate the custom editor with the table's current data
// DO NOTHING HERE - we have already set the data internally
// This calls onSetData
super(editor, index)
})
zombie.OnSetModelData(func(super func(editor *qt.QWidget, model *qt.QAbstractItemModel, index *qt.QModelIndex), editor *qt.QWidget, model *qt.QAbstractItemModel, index *qt.QModelIndex) {
// the table has requested to save data from the editor back into the model
// DO NOTHING HERE - autoconfig{} has already updated the data internally -
// However we do need to trigger model.OnSetData so that the green colour shows?
// FIXME
super(editor, model, index)
})
zombie.OnUpdateEditorGeometry(func(super func(editor *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex), editor *qt.QWidget, option *qt.QStyleOptionViewItem, index *qt.QModelIndex) {
// after having created our custom editor, set the widget's size as per the table column
editor.SetGeometryWithGeometry(option.Rect())
// super(editor, option, index)
})
return zombie.QAbstractItemDelegate
}

46
table_string.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import (
qt "github.com/mappu/miqt/qt6"
)
type stringColumn struct {
vals []string
}
func (sc *stringColumn) SetRowCount(newlen int) {
sc.vals = slice_set_len(sc.vals, newlen)
}
func (sc *stringColumn) SetCell(aRow int, data any) {
str, ok := data.(string)
if !ok {
panic("bad data type")
}
sc.vals[aRow] = str
}
func (sc *stringColumn) Display(aRow int) *qt.QVariant {
return qt.NewQVariant11(sc.vals[aRow])
}
func (sc *stringColumn) Inspect(parent *qt.QWidget, aRow int) {
// do nothing
}
func (sc *stringColumn) SimpleEditData(aRow int) *qt.QVariant {
// Theoretically don't need to return anything here for simple-edits since
// the popup editor works directly with the column-store
return qt.NewQVariant11(sc.vals[aRow])
}
func (sc *stringColumn) ApplySimpleChange(aRow int, data *qt.QVariant) {
sc.vals[aRow] = data.ToString()
}
func (sc *stringColumn) IndicateBlue() bool {
return false
}
var _ TableColumn = &stringColumn{}

View File

@@ -1,5 +1,12 @@
package main package main
import (
"bytes"
"encoding/gob"
"go.mongodb.org/mongo-driver/v2/bson"
)
// box_interface creates a slice where all elements of the input slice are boxed // box_interface creates a slice where all elements of the input slice are boxed
// in an interface{} type. // in an interface{} type.
func box_interface[T any](input []T) []interface{} { func box_interface[T any](input []T) []interface{} {
@@ -10,6 +17,24 @@ func box_interface[T any](input []T) []interface{} {
return ret return ret
} }
func slice_repeat[T any](value T, count int) []T {
ret := make([]T, count)
for i := 0; i < count; i++ {
ret[i] = value
}
return ret
}
func address_of[T any](value T) *T {
return &value
}
func slice_dup[T any](src []T) []T {
ret := make([]T, len(src))
copy(ret, src)
return ret
}
func slice_contains[T comparable](haystack []T, needle T) bool { func slice_contains[T comparable](haystack []T, needle T) bool {
_, ok := slice_find(haystack, needle) _, ok := slice_find(haystack, needle)
return ok return ok
@@ -24,3 +49,71 @@ func slice_find[T comparable](haystack []T, needle T) (int, bool) {
return -1, false return -1, false
} }
func slice_remove_index[T comparable](slice []T, s int) []T {
return append(slice[:s], slice[s+1:]...)
}
// slice_and copies a slice and adds one more thing on the end.
func slice_and[T comparable](base []T, and T) []T {
ret := make([]T, len(base)+1)
copy(ret, base)
ret[len(ret)-1] = and
return ret
}
// slice_apply creates a new slice where every element of the input arr is transformed.
func slice_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
}
// slice_set_len sets the length of the slice. Use it like append -
// x = slice_set_len(x, 10)
func slice_set_len[T any](slice []T, newlen int) []T {
if newlen > len(slice) {
// Extending length
return append(slice, make([]T, newlen-len(slice))...)
}
// Truncating
return slice[0:newlen]
}
func pathlist_encode(bp []string) ([]byte, error) {
buff := bytes.Buffer{}
enc := gob.NewEncoder(&buff)
err := enc.Encode(bp)
if err != nil {
return nil, err
}
return buff.Bytes(), nil
}
func pathlist_decode(input []byte) ([]string, error) {
dec := gob.NewDecoder(bytes.NewReader(input))
var ret []string
err := dec.Decode(&ret)
return ret, err
}
func bson_find_id(doc bson.D) (string, bool) {
for _, pair := range doc {
if pair.Key == "_id" {
return pair.Value.(string), true
}
}
return "", false
}
func alloc_inplace[T any](target **T) {
var newObj T
*target = &newObj
}

33
util_types_test.go Normal file
View File

@@ -0,0 +1,33 @@
package main
import (
"reflect"
"testing"
)
func TestPathlistEncode(t *testing.T) {
// Ensure that invalid UTF-8 data is not corrupted in a pathlist encoding
// roundtrip
input := []string{
"\x00\x01\x02\x03\x04\x05\x06\x07",
"\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7",
"\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7",
"hello",
"world",
}
buff, err := pathlist_encode(input)
if err != nil {
t.Fatal(err)
}
ret, err := pathlist_decode(buff)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(input, ret) {
t.Error("Mismatch")
}
}

Some files were not shown because too many files have changed in this diff Show More